In one sentence

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.

Why it matters

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 big picture

The onion: layers around your controller

🌍 Global middleware (every request: trim strings, maintenance mode…) πŸͺ 'web' group (cookies, session, CSRF) πŸ” Route middleware auth, admin 🎯 Controller PostController@store any layer can stop early, e.g. redirect to /login request response Before-logic runs on the way in (outer β†’ inner); after-logic runs on the way out (inner β†’ outer).
Each layer receives the request, may do something, then calls $next($request) to pass it inward. The response then travels back out through the same layers.
Step by step

Write, register and use middleware

  1. Generate a middleware class

    Let's protect the blog's admin area so only admins get in.

    
              php artisan make:middleware EnsureUserIsAdmin
            

    This creates app/Http/Middleware/EnsureUserIsAdmin.php with a single handle() 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);
                  }
              }
            

    $next is 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.

  2. 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;
                  }
                
  3. Register middleware in bootstrap/app.php

    Laravel 11+ has no Http/Kernel.php. Middleware is configured in bootstrap/app.php through ->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();
            
    MethodWhat it does
    append() / prepend()Add to the global stack (end / start)
    web(append: [...]), api(...)Add to the built-in web or api group
    alias([...])Give a class a short name like 'admin'
    group('name', [...])Bundle several middleware under one name
  4. 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, throttle and signed.

  5. 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
            
Common mistakes

Avoid this, do that

❌ Avoid

          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.

βœ… Do

          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.

❌ Avoid

          $middleware->append(EnsureUserIsAdmin::class);
        

Global middleware runs on every request, including the login page, so nobody could ever log in.

βœ… Do

          $middleware->alias(['admin' => EnsureUserIsAdmin::class]);
          // routes: ->middleware(['auth', 'admin'])
        

Keep access rules on the routes that need them, after auth.

πŸ’‘ Pro tip: see which middleware a route runs

Add -v to the route list to print every middleware attached to each route:


        php artisan route:list -v
      
πŸ‹οΈ Try it yourself
  1. Create EnsureUserIsAdmin and alias it as admin in bootstrap/app.php.
  2. Protect a /admin route with ['auth', 'admin'] and test it as a guest, a normal user and an admin.
  3. Write an after middleware that adds an X-Response-Time header, and append it globally. Check the header in your browser's dev tools.
  4. Build a role middleware that accepts parameters, and use role:editor,admin on a drafts route.
Recap

Key takeaways

Check yourself

Mini quiz