Skip to content
Tech

Laravel Middleware Patterns: Building Clean Request Pipelines

How to structure global, group and route middleware in Laravel so cross-cutting logic stays out of your controllers.

Laravel Middleware Patterns: Building Clean Request Pipelines
Laravel Middleware Patterns: Building Clean Request Pipelines
In this article

    Middleware is one of the clearest examples of a pipeline pattern in everyday web development, but it is easy to misuse. Once a project grows past a handful of routes, teams tend to either stuff everything into one giant middleware class or scatter checks across controllers where middleware should have handled them. Getting the boundaries right makes a Laravel application easier to reason about and test.

    What Middleware Is Actually For

    Middleware exists to handle concerns that apply before or after a request reaches your application logic, independent of what that logic does. Authentication, locale detection, request logging, rate limiting and response header injection are all good fits. Business rules that depend on the specific resource being acted on, such as "only the owner can edit this invoice," usually belong in a policy instead, not middleware, because they need entity-specific context that is awkward to express generically.

    A useful test: if you could describe the check without naming a specific controller or model, it is probably middleware. If the description requires "this particular record," it probably belongs closer to the business logic.

    Global, Group and Route Middleware

    Laravel gives you three scopes, and picking the right one avoids both over-application and repetition. Global middleware in bootstrap/app.php runs on every request, so reserve it for things that truly apply everywhere, such as trimming strings or handling CORS preflight. Group middleware (the web and api groups) covers session handling, CSRF and throttling for a whole class of routes. Route-level middleware is for anything specific to a handful of endpoints.

    // bootstrap/app.php
    return Application::configure(basePath: dirname(__DIR__))
        ->withMiddleware(function (Middleware $middleware) {
            $middleware->web(append: [
                \App\Http\Middleware\SetLocaleFromSession::class,
            ]);
    
            $middleware->alias([
                'ability' => \App\Http\Middleware\EnsureTokenAbility::class,
            ]);
        })
        ->create();

    Keeping the alias list in one place makes it trivial to see every custom middleware a route could reference, instead of hunting through route files for raw class references.

    Passing Parameters to Middleware

    Middleware parameters let you reuse one class for several similar checks instead of writing near-duplicate classes. A common case is restricting a route to specific roles or token abilities.

    class EnsureTokenAbility
    {
        public function handle(Request $request, Closure $next, string ...$abilities): Response
        {
            $token = $request->user()?->currentAccessToken();
    
            if (! $token || ! collect($abilities)->every(fn ($a) => $token->can($a))) {
                abort(403, 'Missing required ability.');
            }
    
            return $next($request);
        }
    }

    Registered as 'ability' => EnsureTokenAbility::class, it can then be attached to a route as ability:reports.export,reports.view. This keeps the authorization rule declarative and visible right in the route definition, rather than buried inside a controller method.

    Terminable Middleware for Post-Response Work

    Some work genuinely needs to happen after the response has already been sent to the client — flushing metrics, writing audit logs, or closing a long-running resource. Laravel's terminable middleware runs a terminate() method after the response is sent to the browser, which keeps that work from adding to perceived request latency.

    class RecordRequestMetrics
    {
        public function handle(Request $request, Closure $next): Response
        {
            $request->attributes->set('started_at', microtime(true));
    
            return $next($request);
        }
    
        public function terminate(Request $request, Response $response): void
        {
            $duration = microtime(true) - $request->attributes->get('started_at');
    
            Metrics::timing('http.request', $duration, [
                'route' => $request->route()?->getName() ?? 'unknown',
                'status' => $response->getStatusCode(),
            ]);
        }
    }
    Tip: Terminable middleware only runs reliably with the standard web server lifecycle. If you deploy with Octane or FPM workers that reuse the process, double-check that nothing in terminate() depends on request-scoped state leaking across requests.

    Common Pitfalls

    • Order matters. Middleware runs in the order it is registered, and the "after" phase runs in reverse. A rate limiter placed after an expensive database lookup still lets that lookup run before rejecting the request.
    • Avoid side effects in constructors. Middleware classes are resolved from the container, so heavy work in the constructor runs on every matching request even when handle() short-circuits early.
    • Don't duplicate what policies already do. If a middleware class starts needing a model ID from the route to make its decision, that is usually a sign the check belongs in a policy or form request instead.
    Middleware should answer "is this request allowed to proceed at all," not "is this specific record allowed to be touched this way."

    Wrapping Up

    Treat middleware as a pipeline for concerns that genuinely apply regardless of which controller handles the request. Keep global middleware minimal, use groups for broad categories, and reach for parameterized or terminable middleware when it removes duplication rather than when it is merely convenient. The result is a request lifecycle that stays easy to trace from the route file alone.

    Written by Raj

    Founder & Editor

    I write HorizonDesk: practical, no-hype articles on technology, money, business and the vehicles we drive. Everything here is something I’ve researched, tested or wanted explained clearly myself.