Back to Blogs
2026-07-07 Abdelrhman Yasser

Middleware in Laravel: From Zero to Production

Middleware in Laravel: Request Filtering & Pipeline Architecture

Reading Time: 25 minutes

Introduction

Middleware provides a powerful mechanism for filtering and manipulating HTTP requests entering your application. Think of it as a series of layers — each can inspect, modify, or reject the request before it reaches your application, and can also modify the response on its way back.

Laravel's middleware system is built on the Pipeline design pattern, which chains handlers together so each passes the request to the next.

The Problem

Without middleware, every controller would need to repeat the same boilerplate:

class PostController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        if (!auth()->check()) {
            return response()->json(['error' => 'Unauthenticated'], 401);
        }

        if (auth()->user()->cannot('create', Post::class)) {
            return response()->json(['error' => 'Forbidden'], 403);
        }

        $validated = validator($request->all(), [
            'title' => 'required|string|max:255',
        ])->validate();

        // ... actual business logic
    }
}

Middleware extracts cross-cutting concerns into reusable, testable classes.

How Middleware Works

sequenceDiagram
    participant C as Client
    participant M1 as Middleware 1
    participant M2 as Middleware 2
    participant M3 as Middleware 3
    participant R as Route/Controller

    C->>M1: Request
    M1->>M2: Request
    M2->>M3: Request
    M3->>R: Request
    R-->>M3: Response
    M3-->>M2: Response
    M2-->>M1: Response
    M1-->>C: Response

Each middleware receives the request, performs its logic, then calls $next($request) to pass it to the next layer.

Creating Middleware

php artisan make:middleware EnsureTokenIsValid
class EnsureTokenIsValid
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->header('X-API-Key') !== config('app.api_key')) {
            return response()->json(['error' => 'Invalid API key'], 401);
        }

        return $next($request);
    }
}

Before vs After Middleware

Before middleware — runs before the request reaches the controller:

class BeforeMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        // Perform action before request
        Log::info('Request started', ['url' => $request->url()]);

        return $next($request);
    }
}

After middleware — runs after the controller returns a response:

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

        // Perform action after response is generated
        Log::info('Request completed', [
            'url' => $request->url(),
            'status' => $response->status(),
        ]);

        return $response;
    }
}

Registering Middleware

Global Middleware

Runs on every HTTP request. Defined in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->append(EnsureTokenIsValid::class);
    $middleware->prepend(TrustHosts::class);
})

Route Middleware

Assigned to specific routes or groups. Registered in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'auth' => Authenticate::class,
        'verified' => EnsureEmailIsVerified::class,
        'throttle' => ThrottleRequests::class,
    ]);
})

Usage on routes:

Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/profile', [ProfileController::class, 'edit']);
});

Middleware Groups

Groups bundle multiple middleware under one key:

->withMiddleware(function (Middleware $middleware) {
    $middleware->group('api', [
        'throttle:api',
        EnsureTokenIsValid::class,
    ]);
})

Laravel ships with two default groups: web and api.

Middleware Parameters

Middleware can accept additional parameters:

class CheckRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (!auth()->check()) {
            return redirect('login');
        }

        foreach ($roles as $role) {
            if (auth()->user()->hasRole($role)) {
                return $next($request);
            }
        }

        abort(403, 'Unauthorized action.');
    }
}

Registration:

$middleware->alias(['role' => CheckRole::class]);

Usage:

Route::get('/admin', function () { ... })->middleware('role:admin,super-admin');

TrustProxies Middleware

Critical for applications behind load balancers or reverse proxies:

class TrustProxies extends Middleware
{
    protected $proxies = '*';

    protected $headers = Request::HEADER_X_FORWARDED_FOR |
                         Request::HEADER_X_FORWARDED_HOST |
                         Request::HEADER_X_FORWARDED_PORT |
                         Request::HEADER_X_FORWARDED_PROTO |
                         Request::HEADER_X_FORWARDED_AWS_ELB;
}

Without this, url()->secure(), redirect()->intended(), and rate limiting may misbehave behind a proxy.

Rate Limiting Middleware

Laravel's throttle middleware prevents abuse:

// Basic rate limiting
Route::middleware('throttle:60,1')->group(function () {
    Route::get('/api/users', [UserController::class, 'index']);
});

// Named rate limiters (defined in AppServiceProvider or RouteServiceProvider)
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

// Dynamic limits based on user
RateLimiter::for('premium-api', function (Request $request) {
    if ($request->user()?->isPremium()) {
        return Limit::perMinute(1000);
    }
    return Limit::perMinute(60);
});

// Burst limiting
RateLimiter::for('burst', function (Request $request) {
    return [
        Limit::perMinute(60),
        Limit::perSecond(5),
    ];
});

Custom Rate Limiter Responses

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)
        ->by($request->ip())
        ->response(function (Request $request, array $headers) {
            return response()->json([
                'message' => 'Too many requests. Please slow down.',
                'retry_after' => $headers['Retry-After'] ?? 60,
            ], 429);
        });
});

Sortable Middleware

Control the order middleware runs within its group:

->withMiddleware(function (Middleware $middleware) {
    $middleware->appendToGroup('web', EnsureTokenIsValid::class);

    $middleware->prependToGroup('web', CheckForMaintenanceMode::class);
})

Priority

For advanced ordering, use priority:

->withMiddleware(function (Middleware $middleware) {
    $middleware->priority([
        StartSession::class,
        ShareErrorsFromSession::class,
        EncryptCookies::class,
    ]);
})

Terminable Middleware

Middleware that runs after the response has been sent to the browser:

class TerminatingMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    public function terminate(Request $request, Response $response): void
    {
        // Log, queue notifications, close connections
        Log::info('Request fully terminated', [
            'memory' => memory_get_peak_usage(true),
        ]);
    }
}

Great for:

  • Logging — request duration and memory usage
  • Cleanup — closing connections, flushing temporary state
  • Deferred work — dispatching jobs you want to fire after the response

Built-in Middleware Reference

MiddlewarePurpose
Authenticate (auth)Ensure user is logged in
EnsureEmailIsVerified (verified)Redirect unverified users
ThrottleRequests (throttle)Rate limit routes
EncryptCookiesEncrypt all cookies
TrimStringsTrim whitespace from input
TrustProxiesConfigure trusted proxies
ValidateCsrfTokenCSRF protection
SubstituteBindingsReplace route params with models
RedirectIfAuthenticated (guest)Redirect logged-in users

Common Mistakes

Mistake 1: Order Dependency in Global Middleware

// ❌ Session-dependent middleware before sessions start
$middleware->append(CheckUserRole::class);

// ✅ Ensure session is active first
$middleware->appendToGroup('web', CheckUserRole::class);

Mistake 2: Heavy Operations in Global Middleware

// ❌ Every request pays this cost
class GlobalMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = User::with('permissions')->find(auth()->id()); // DB query
        View::share('currentUser', $user); // Memory

        return $next($request);
    }
}

Fix: Use lazy loading or view composers instead.

Mistake 3: Ignoring CSRF on API Routes

// ❌ CSRF middleware on stateless API (will fail for non-browser clients)
Route::middleware('api')->group(function () {
    // API routes still have VerifyCsrfToken by default if not in api group
});

Fix: API routes should use the api middleware group which excludes CSRF.

Mistake 4: Not Using Named Rate Limiters

// ❌ Inline throttle is hard to manage
Route::get('/api/export', ...)->middleware('throttle:10,1');

// ✅ Named limiters are reusable and testable
RateLimiter::for('export', fn ($req) => Limit::perMinute(10));
Route::get('/api/export', ...)->middleware('throttle:export');

Best Practices Summary

  1. Use middleware for cross-cutting concerns — auth, logging, rate limiting, CORS
  2. Keep middleware focused — one responsibility per middleware
  3. Use middleware groups over global — don't slow down every request
  4. Register named rate limiters — testable and reusable
  5. Use terminable middleware for post-response work — don't delay the response
  6. Use prepend and priority — control execution order explicitly
  7. Test middleware in isolation — use $this->call() with middleware

Interview Questions

  1. Explain the middleware pipeline and how $next($request) works.
  2. What is the difference between global, route, and group middleware?
  3. How do middleware parameters work?
  4. What does TrustProxies middleware do and why is it important?
  5. How would you implement role-based access control with middleware?
  6. Explain the terminate() method and when you would use it.
  7. How does the throttle middleware work under the hood?

Related Articles

# Laravel# PHP# Middleware# Security# Backend
WhatsApp
Chat on WhatsApp