Middleware is code that wraps around your controller: every request passes through it on the way in, and every response passes back through it on the way out.
πThink of airport security. Before you reach the gate (the controller), you pass a ticket check, then passport control, then the scanner. Any checkpoint can turn you away. On the way home, you pass through customs in reverse order.
One check, every route
Guard whole areas
Login checks, admin-only sections, email verification. Write the rule once and attach it to many routes.
Keep controllers clean
Controllers focus on their job; cross-cutting concerns like locale, headers and logging live in middleware.
Laravel relies on it
Sessions, cookie encryption, CSRF protection and rate limiting are all middleware you already use.
The onion: layers around your controller
$next($request) to pass it inward. The response then travels back out through the same layers.Write, register and use middleware
-
Generate a middleware class
Let's protect the blog's admin area so only admins get in.
php artisan make:middleware EnsureUserIsAdminThis creates
app/Http/Middleware/EnsureUserIsAdmin.phpwith a singlehandle()method:<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class EnsureUserIsAdmin { public function handle(Request $request, Closure $next): Response { if (! $request->user()?->is_admin) { abort(403, 'Admins only.'); } return $next($request); } }$nextis a closure that represents "the rest of the onion". Calling$next($request)passes the request to the next layer and gives you back the response. If you don't call it, the request stops right here. -
Before vs after middleware
Where you put your code relative to
$next()decides when it runs:Before middleware
Acts on the request, before the controller runs.
public function handle(Request $request, Closure $next): Response { app()->setLocale($request->user()?->locale ?? 'en'); return $next($request); }After middleware
Acts on the response, after the controller has run.
public function handle(Request $request, Closure $next): Response { $response = $next($request); $response->headers->set('X-Frame-Options', 'DENY'); return $response; } -
Register middleware in
bootstrap/app.phpLaravel 11+ has no
Http/Kernel.php. Middleware is configured inbootstrap/app.phpthrough->withMiddleware():use App\Http\Middleware\AddSecurityHeaders; use App\Http\Middleware\EnsureUserIsAdmin; use App\Http\Middleware\SetLocale; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { // Global: runs on EVERY request $middleware->append(AddSecurityHeaders::class); // Add to the existing 'web' group $middleware->web(append: [SetLocale::class]); // A short name to use on routes $middleware->alias([ 'admin' => EnsureUserIsAdmin::class, ]); // Your own named group $middleware->group('editor-area', [ 'auth', 'verified', EnsureUserIsAdmin::class, ]); }) // ->withRouting(...) ->withExceptions(...) ->create();Method What it does append()/prepend()Add to the global stack (end / start) web(append: [...]),api(...)Add to the built-in weborapigroupalias([...])Give a class a short name like 'admin'group('name', [...])Bundle several middleware under one name -
Assign middleware to routes
Attach by alias, by class name or by group. Groups of routes share middleware, just like in Routing:
Route::get('/admin', AdminDashboardController::class) ->middleware(['auth', 'admin']); Route::middleware(['auth', EnsureUserIsAdmin::class])->group(function () { Route::resource('categories', CategoryController::class); }); // Skip one middleware for a single route Route::post('/webhooks/stripe', StripeWebhookController::class) ->withoutMiddleware([ValidateCsrfToken::class]);Built-in aliases you'll use constantly:
auth,guest,verified,can,throttleandsigned. -
Pass parameters to middleware
Add a colon after the name, and separate several values with commas. They arrive as extra arguments after
$next:public function handle(Request $request, Closure $next, string ...$roles): Response { if (! in_array($request->user()?->role, $roles, true)) { abort(403); } return $next($request); }Route::get('/drafts', [DraftController::class, 'index']) ->middleware('role:editor,admin'); // $roles = ['editor', 'admin'] Route::post('/comments', [CommentController::class, 'store']) ->middleware('throttle:10,1'); // built-in: 10 per minute
Avoid this, do that
public function handle(Request $request, Closure $next): Response
{
if ($request->user()->is_admin) {
$next($request);
}
}
Forgetting to return $next($request) (and calling user() on guests) leads to blank pages and null errors.
public function handle(Request $request, Closure $next): Response
{
abort_unless($request->user()?->is_admin, 403);
return $next($request);
}
Always return a response, and use ?-> because the user might not be logged in.
$middleware->append(EnsureUserIsAdmin::class);
Global middleware runs on every request, including the login page, so nobody could ever log in.
$middleware->alias(['admin' => EnsureUserIsAdmin::class]);
// routes: ->middleware(['auth', 'admin'])
Keep access rules on the routes that need them, after auth.
Add -v to the route list to print every middleware attached to each route:
php artisan route:list -v
- Create
EnsureUserIsAdminand alias it asadmininbootstrap/app.php. - Protect a
/adminroute with['auth', 'admin']and test it as a guest, a normal user and an admin. - Write an after middleware that adds an
X-Response-Timeheader, and append it globally. Check the header in your browser's dev tools. - Build a
rolemiddleware that accepts parameters, and userole:editor,adminon a drafts route.
Key takeaways
- Middleware wraps your controller like layers of an onion.
handle($request, Closure $next)must return a response, usually$next($request).- Code before
$next()= before middleware; code after = after middleware. - Laravel 11+ registers middleware in
bootstrap/app.phpvia->withMiddleware():append,alias,group. - Attach to routes with
->middleware('alias'); pass parameters as'name:a,b'.