Middleware sits between an incoming HTTP request and the controller that ultimately handles it — a filtering and modification layer that can inspect the request, reject it outright (redirecting to a login page, returning a 429 rate-limit response), or pass it through, and can do the same to the response on the way back out.
1. A concrete example: the auth middleware
Route::get('/dashboard', [DashboardController::class, 'index'])->middleware('auth');
Before DashboardController::index() ever runs, the auth middleware checks whether the current user is authenticated — if not, it redirects to the login page and DashboardController::index() never executes at all. The controller itself contains no authentication-checking code; that concern is handled entirely by the middleware layer sitting in front of it.
2. Writing custom middleware
php artisan make:middleware EnsureUserIsAdmin
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next)
{
if (! $request->user()?->is_admin) {
abort(403, 'Admins only.');
}
return $next($request);
}
}
$next($request) is what actually passes the request further down the chain — toward either the next middleware in line, or the controller itself once every middleware has run. Not calling $next() at all (as with the abort() call above, which halts execution) stops the request right there, and the controller never runs.
3. Registering and applying it
// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
]);
})
Route::get('/admin/dashboard', [AdminController::class, 'index'])->middleware(['auth', 'admin']);
Multiple middleware entries run in the order listed — here, auth runs first (confirming the user is logged in at all), then admin (confirming that logged-in user specifically has admin privileges). Order matters when one middleware's logic depends on another having already run — checking admin status before confirming a user is even authenticated wouldn't make sense.
4. Modifying the response on the way back out
public function handle(Request $request, Closure $next)
{
$response = $next($request); // let the request continue through the chain first
$response->headers->set('X-Custom-Header', 'value');
return $response;
}
Code placed after the $next($request) call runs on the way back out, once the controller (and any deeper middleware) has already produced a response — this is how middleware can modify an outgoing response, adding headers or logging its status, rather than only intercepting the incoming request.
5. Global vs. route-specific middleware
// bootstrap/app.php — runs on literally every request, no route assignment needed
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\TrackPageViews::class);
})
Global middleware runs on every single request regardless of route — appropriate for something that genuinely applies app-wide (basic logging, a maintenance-mode check). Anything that should only apply to specific routes belongs as route-specific middleware instead, applied explicitly with ->middleware() the way auth is above — applying route-specific logic globally just means adding conditional logic inside the middleware to skip most requests, which is usually the wrong layer for that decision.
6. Common built-in middleware worth knowing
auth // requires an authenticated user
guest // requires the OPPOSITE — an unauthenticated visitor (used on login/register routes)
throttle:60,1 // rate limits to 60 requests per minute
verified // requires a verified email address
Several of these ship with Laravel by default and cover the most common request-filtering needs directly — custom middleware is generally for application-specific logic (like the admin check above) that doesn't correspond to one of these standard, already-solved cases.