Table of Contents
Laravel middleware sits between an incoming request and your controller. Every request your application receives passes through middleware before anything else runs. That makes it the right place for logic that applies across multiple routes – authentication checks, role verification, rate limiting, request logging.
This guide covers how Laravel middleware works internally, creating custom middleware, protecting routes, building role-based access control, and the patterns that keep middleware maintainable as applications grow.
Prerequisites
- Laravel 10 or higher installed
- Basic understanding of Laravel routing
- Familiarity with Laravel controllers
How Laravel Middleware Works
Every HTTP request in Laravel travels through a pipeline of middleware before reaching the controller. Think of it as a series of checkpoints – each middleware either allows the request to continue or returns a response immediately:
Request
→ Global Middleware (runs on every request)
→ Route Middleware (runs on specific routes)
→ Controller
→ Response
→ Route Middleware (runs after controller)
→ Global Middleware (runs after controller)
→ Browser
Middleware can run before the controller (inspecting or modifying the request), after the controller (inspecting or modifying the response), or both. Most middleware runs before.
Built-in Laravel Middleware
Laravel ships with several middleware you use immediately without creating anything:
// Protect a route - redirect guests to login page
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware('auth');
// Guest-only route - redirect authenticated users away
Route::get('/login', function () {
return view('login');
})->middleware('guest');
// Throttle requests - max 60 per minute
Route::get('/api/data', function () {
return response()->json(['data' => 'value']);
})->middleware('throttle:60,1');
// Verify email before access
Route::get('/settings', function () {
return view('settings');
})->middleware(['auth', 'verified']);
The auth middleware is the most commonly used. It checks for an authenticated session and redirects unauthenticated users to the login page automatically.
Creating Custom Middleware
Use Artisan to generate middleware – never create the file manually:
php artisan make:middleware CheckAge
Output:
INFO Middleware [app/Http/Middleware/CheckAge.php] created successfully.
Open the generated file:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckAge
{
public function handle(Request $request, Closure $next): Response
{
// Logic runs BEFORE the controller
if ($request->age < 18) {
return redirect('/home')->with('error', 'You must be 18 or older.');
}
// Pass the request to the next middleware or controller
return $next($request);
}
}
The $next($request) call is what passes the request forward. Without it the request stops here and the controller never runs.
Registering Middleware
In Laravel 10 register middleware in app/Http/Kernel.php. In Laravel 11 use bootstrap/app.php:
// Laravel 10 - app/Http/Kernel.php
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'check.age' => \App\Http\Middleware\CheckAge::class, // add this
];
// Laravel 11 - bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'check.age' => \App\Http\Middleware\CheckAge::class,
]);
})
Now use the alias in routes:
Route::get('/adults-only', function () {
return view('content');
})->middleware('check.age');
Before and After Middleware
Middleware runs before the controller by default. To run logic after the controller returns a response, capture the response first:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class LogRequests
{
public function handle(Request $request, Closure $next): Response
{
// BEFORE: runs before controller
$startTime = microtime(true);
// Pass to controller
$response = $next($request);
// AFTER: runs after controller returns response
$duration = round((microtime(true) - $startTime) * 1000, 2);
\Log::info("Request: {$request->method()} {$request->path()} - {$duration}ms");
return $response;
}
}
Middleware Groups
Apply multiple middleware to a group of routes without repeating them on every route:
// Apply auth and verified to all routes in the group
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::get('/profile', [ProfileController::class, 'show']);
Route::get('/settings', [SettingsController::class, 'index']);
Route::post('/settings', [SettingsController::class, 'update']);
});
// Prefix + middleware together
Route::middleware(['auth'])->prefix('admin')->group(function () {
Route::get('/', [AdminController::class, 'index']);
Route::get('/users', [AdminController::class, 'users']);
Route::get('/reports',[AdminController::class, 'reports']);
});
Real-World Example: Role-Based Access Control
Protecting routes by user role is one of the most common middleware use cases in real applications:
php artisan make:middleware CheckRole
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckRole
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
// User must be authenticated first
if (!auth()->check()) {
return redirect('/login');
}
$userRole = auth()->user()->role;
// Check if user has any of the required roles
if (!in_array($userRole, $roles)) {
abort(403, 'Unauthorized. Required role: ' . implode(' or ', $roles));
}
return $next($request);
}
}
Register it:
// Kernel.php (Laravel 10)
'role' => \App\Http\Middleware\CheckRole::class,
Use it in routes – passing the required role as a parameter:
// Only admins
Route::get('/admin', [AdminController::class, 'index'])
->middleware('role:admin');
// Admins or managers
Route::get('/reports', [ReportController::class, 'index'])
->middleware('role:admin,manager');
// Multiple middleware - auth first, then role check
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin/dashboard', [AdminController::class, 'dashboard']);
Route::get('/admin/users', [AdminController::class, 'users']);
});
Passing Parameters to Middleware
The role example above accepts parameters. Here’s how the parameter passing works:
// Route definition passes the parameter after colon
->middleware('role:admin')
// Middleware handle method receives it as an argument
public function handle(Request $request, Closure $next, string $role): Response
{
// $role = 'admin'
}
// Multiple parameters separated by comma
->middleware('role:admin,manager')
// Middleware receives as variadic argument
public function handle(Request $request, Closure $next, string ...$roles): Response
{
// $roles = ['admin', 'manager']
}
Global Middleware
Some middleware should run on every single request – security headers, maintenance mode, CORS handling. Register these globally:
// Laravel 10 - app/Http/Kernel.php
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\AddSecurityHeaders::class, // your custom middleware
];
// Laravel 11 - bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append(
\App\Http\Middleware\AddSecurityHeaders::class
);
})
Debugging Middleware
When middleware isn’t behaving as expected these tools diagnose the problem quickly.
Check that your middleware is registered and routes are using it:
php artisan route:list
The Middleware column shows which middleware each route uses. If it’s blank when you expected middleware, check your route definition.
Dump the request inside middleware to inspect what’s coming in:
<?php
public function handle(Request $request, Closure $next): Response
{
// Temporary debugging - remove before production
dd([
'user' => auth()->user(),
'path' => $request->path(),
'method' => $request->method(),
'headers' => $request->headers->all(),
]);
return $next($request);
}
Clear cached routes when changes don’t take effect:
php artisan route:clear
php artisan config:clear
php artisan cache:clear
Best Practices
- One responsibility per middleware – authentication middleware checks authentication. Role middleware checks roles. Don’t combine them into one class.
- Keep middleware lightweight – middleware runs on every matched request. Heavy database queries or external API calls in middleware add latency to every response.
- Order matters – middleware in a chain runs in order. Put authentication before authorization – there’s no point checking role if the user isn’t logged in.
- Use groups for shared middleware – don’t repeat the same middleware on every route individually. Group related routes and apply middleware once.
- Use abort() for unauthorized access –
abort(403)is cleaner than manual redirects and integrates with Laravel’s error handling automatically.
Frequently Asked Questions
What is the difference between middleware and a controller in Laravel?
Middleware runs before (and optionally after) the controller handles a request. It’s for logic that applies to multiple routes – authentication, rate limiting, logging. Controllers handle specific request logic – what happens when a particular route is accessed. If the same check appears on five different routes, it belongs in middleware.
Can I apply multiple middleware to a single route?
Yes – pass an array:
Route::get('/admin', [AdminController::class, 'index'])
->middleware(['auth', 'verified', 'role:admin']);
Middleware runs in the order listed. Auth checks authentication first, verified checks email verification second, role checks the admin role third.
What is the difference between global middleware and route middleware?
Global middleware runs on every request regardless of which route is matched – typically used for security headers, CORS, and maintenance mode. Route middleware only runs when explicitly assigned to a route or group – used for authentication, authorization, and route-specific logic.
Why is my middleware not running?
Three common causes. First, middleware isn’t registered – check Kernel.php (Laravel 10) or bootstrap/app.php (Laravel 11) for the alias. Second, the alias in the route doesn’t match the registered alias – both must be identical. Third, cached routes are being served – run php artisan route:clear and try again.
Should middleware redirect or abort on failure?
Use redirect for user-facing failures where the user can take action – redirect to login when unauthenticated, redirect to home when under age. Use abort(403) for authorization failures where the user is authenticated but not allowed – it returns a proper HTTP error response and integrates with Laravel’s error pages.
Summary
Laravel middleware gives you a clean place to put logic that applies across multiple routes. The key points:
- Middleware runs before controllers – inspect and modify requests, or return a response immediately to stop the chain
- Register with an alias – Kernel.php in Laravel 10, bootstrap/app.php in Laravel 11, then use the alias in routes
- Use groups – apply shared middleware once to a group rather than repeating on every route
- Parameters work via colon syntax –
middleware('role:admin')passes “admin” as an argument to the handle method - Order matters in chains – authenticate before authorizing, always
For connecting middleware to the controllers it protects, the Laravel controllers guide covers controller creation, routing, and request handling in detail. For a complete working application using middleware, controllers, and models together, the Laravel CRUD guide builds a full feature end to end.
For the full middleware API reference see the official Laravel middleware documentation.
