Introduction

Middleware is Laravel's pipeline for cross-cutting concerns: authentication, CSRF, logging, rate limiting, header injection. This lesson explains the onion model, how to write custom middleware, and how to register and assign it in bootstrap/app.php.

Key Concepts

  • Middleware: A class with a handle($request, Closure $next) method that wraps the controller.
  • Onion model: Code before $next($request) runs on the way in; code after runs on the way out.
  • Global vs route middleware: Global runs on every request; route middleware is attached to individual routes.
  • bootstrap/app.php: Central place to register aliases and add middleware to groups (Laravel 11+).
  • Alias: A short name for a middleware class that you can reference from routes.

Real World Context

Every auth check, every rate limit, every CORS header comes from middleware. Understanding it is the difference between 'I added it in the controller' (and forgot it elsewhere) and 'I added it to the middleware pipeline' (and it applies everywhere).

Deep Dive

Middleware provides a mechanism for filtering HTTP requests entering your application. Think of middleware as a series of "layers" that requests must pass through before reaching your application.

What is Middleware?

Middleware acts like a gatekeeper:

                    Request
                       │
                       ▼
               ┌───────────────┐
               │  Middleware 1 │  Check if authenticated
               └───────────────┘
                       │
                       ▼
               ┌───────────────┐
               │  Middleware 2 │  Verify CSRF token
               └───────────────┘
                       │
                       ▼
               ┌───────────────┐
               │  Middleware 3 │  Log request
               └───────────────┘
                       │
                       ▼
               ┌───────────────┐
               │  Controller   │  Handle request
               └───────────────┘
                       │
                       ▼
                   Response

Common uses for middleware:

  • Authentication: Is the user logged in?
  • Authorization: Can the user access this resource?
  • CSRF Protection: Is this a legitimate form submission?
  • Rate Limiting: Has this user made too many requests?
  • Logging: Record request information
  • CORS: Add cross-origin headers

Creating Middleware

bash
php artisan make:middleware EnsureUserIsAdmin

This creates app/Http/Middleware/EnsureUserIsAdmin.php:

php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserIsAdmin
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): Response
    {
        if (! $request->user()?->isAdmin()) {
            return redirect('home');
        }

        return $next($request);
    }
}

Middleware Flow

The $next closure passes the request to the next layer:

php
public function handle(Request $request, Closure $next): Response
{
    // BEFORE: Code here runs before the controller
    $startTime = microtime(true);

    // Pass to next middleware (eventually reaching controller)
    $response = $next($request);

    // AFTER: Code here runs after the controller
    $duration = microtime(true) - $startTime;
    Log::info("Request took {$duration} seconds");

    return $response;
}

Before vs After Middleware

Before Middleware

Runs before the controller:

php
public function handle(Request $request, Closure $next): Response
{
    // Perform action BEFORE request is handled
    if ($this->isBlacklisted($request->ip())) {
        abort(403);
    }

    return $next($request);
}
After Middleware

Runs after the controller:

php
public function handle(Request $request, Closure $next): Response
{
    $response = $next($request);

    // Perform action AFTER request is handled
    $response->header('X-Custom-Header', 'Value');

    return $response;
}

Registering Middleware

Configure middleware in bootstrap/app.php:

php
use App\Http\Middleware\EnsureUserIsAdmin;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // Alias for route middleware
        $middleware->alias([
            'admin' => EnsureUserIsAdmin::class,
        ]);
    })
    ->create();

Assigning Middleware to Routes

php
// Single middleware
Route::get('/admin', [AdminController::class, 'index'])
    ->middleware('admin');

// Multiple middleware
Route::get('/admin/users', [AdminController::class, 'users'])
    ->middleware(['auth', 'admin']);

// To a group
Route::middleware(['auth', 'admin'])->group(function () {
    Route::get('/admin/dashboard', ...);
    Route::get('/admin/settings', ...);
});

// Inline middleware class
Route::get('/profile', fn() => ...)->middleware(EnsureUserIsAdmin::class);

Excluding Middleware

php
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', ...);

    // Exclude auth for this route
    Route::get('/public-page', ...)->withoutMiddleware(['auth']);
});

Global Middleware

Run on every request:

php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(LogRequests::class);  // Add to end
    $middleware->prepend(StartTimer::class);  // Add to beginning
})

Middleware Groups

Laravel ships with two default groups — web (sessions, cookies, CSRF) and api (stateless). You rarely redefine them from scratch; instead you append, prepend, or remove entries:

php
->withMiddleware(function (Middleware $middleware) {
    // Add a middleware to the end of the web group
    $middleware->web(append: [\App\Http\Middleware\EnsureUserIsSubscribed::class]);

    // Add a middleware to the beginning of the api group
    $middleware->api(prepend: [\App\Http\Middleware\EnsureTokenIsValid::class]);
});

For full control, you can redefine a group entirely. The defaults Laravel 13 ships with look like this:

php
$middleware->group('web', [
    \Illuminate\Cookie\Middleware\EncryptCookies::class,
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class,
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
]);

$middleware->group('api', [
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
]);

Note: Laravel 13 renamed the CSRF middleware from VerifyCsrfToken to PreventRequestForgery and added origin-aware verification. The @csrf Blade directive and the _token field still work exactly the same.

Common Pitfalls

  1. Blocking the request instead of aborting — return redirect(...) works, but abort(403) is clearer for 'this should not happen' cases.
  2. Putting logic after $next when you meant before — The code runs on the way out, AFTER the controller, which is rarely what you want for authorisation.

Best Practices

  1. Register aliases in bootstrap/app.php — Makes route definitions short and self-documenting.
  2. Keep middleware single-purpose — One responsibility per class. Compose them in route groups.

Summary

  • Middleware wraps controllers in before/after layers.
  • $next($request) passes control to the next layer.
  • Register aliases in bootstrap/app.php via withMiddleware.
  • Global middleware runs on every request; route middleware is opt-in.
  • Keep each middleware focused on one concern.
✓ Completed