Laravel Middleware: How to Filter HTTP Requests (With Examples)

Published on November 17, 2025
Laravel Middleware HTTP Security Authentication PHP

What is Laravel Middleware?

Middleware acts as a filter for HTTP requests entering your application. Think of it as a series of security checkpoints that requests must pass through before reaching your application logic, and responses must pass through before being sent back to the user.

Real-world Analogies:

  • Security Checkpoint: Verifying credentials before allowing entry
  • Logging System: Recording who accessed what and when
  • Traffic Cop: Directing requests to appropriate routes
  • Content Filter: Modifying requests or responses

How Middleware Works

Request Lifecycle with Middleware

HTTP Request 
    ↓
Middleware 1 (e.g., Authenticate)
    ↓
Middleware 2 (e.g., Log Request)  
    ↓
Middleware 3 (e.g., Check Permission)
    ↓
Your Application Logic (Controller)
    ↓
Middleware 3 (Modify Response)
    ↓
Middleware 2 (Log Response)
    ↓
Middleware 1 (Add Headers)
    ↓
HTTP Response

Types of Middleware

  1. Global Middleware - Runs on every request
  2. Route Middleware - Runs on specific routes
  3. Middleware Groups - Bundles of middleware
  4. Terminable Middleware - Runs after response is sent

Creating Custom Middleware

Generating Middleware

# Create a new middleware
php artisan make:middleware CheckUserAge
php artisan make:middleware AdminAccess
php artisan make:middleware LogRequests

Basic Middleware Structure

<?php
// app/Http/Middleware/CheckUserAge.php

namespace App\Http\Middleware;

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

class CheckUserAge
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next): Response
    {
        // Your middleware logic here
        // This runs BEFORE the request is handled
        
        $response = $next($request);
        
        // This runs AFTER the request is handled
        // You can modify the response here
        
        return $response;
    }
}

Practical Middleware Examples

Example 1: Age Verification Middleware

<?php
// app/Http/Middleware/CheckUserAge.php

namespace App\Http\Middleware;

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

class CheckUserAge
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next, $minAge = 18): Response
    {
        $age = $request->input('age') ?? $request->user()->age ?? null;

        if (!$age || $age < $minAge) {
            // Redirect if age requirement not met
            return redirect()->route('age.verification')
                           ->with('error', "You must be at least {$minAge} years old to access this page.");
        }

        return $next($request);
    }
}

Example 2: Admin Access Middleware

<?php
// app/Http/Middleware/AdminAccess.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Auth;

class AdminAccess
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next, $role = 'admin'): Response
    {
        // Check if user is authenticated
        if (!Auth::check()) {
            return redirect()->route('login')
                           ->with('error', 'Please log in to access this page.');
        }

        // Check if user has the required role
        $user = Auth::user();
        if (!$user->hasRole($role)) {
            // If API request, return JSON response
            if ($request->expectsJson()) {
                return response()->json([
                    'error' => 'Unauthorized. Admin access required.'
                ], 403);
            }

            // For web requests, redirect with error
            return redirect()->route('home')
                           ->with('error', 'You do not have permission to access this area.');
        }

        return $next($request);
    }
}

Example 3: Request Logging Middleware

<?php
// app/Http/Middleware/LogRequests.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Log;

class LogRequests
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): Response
    {
        // Log the incoming request
        Log::info('Incoming Request', [
            'method' => $request->method(),
            'url' => $request->fullUrl(),
            'ip' => $request->ip(),
            'user_agent' => $request->userAgent(),
            'user_id' => auth()->id() ?? 'guest',
        ]);

        // Process the request
        $response = $next($request);

        // Log the response
        Log::info('Outgoing Response', [
            'status' => $response->getStatusCode(),
            'url' => $request->fullUrl(),
        ]);

        return $response;
    }
}

Example 4: API Throttling Middleware

<?php
// app/Http/Middleware/ThrottleAPI.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Cache\RateLimiter;
use Illuminate\Support\Facades\Log;

class ThrottleAPI
{
    protected $limiter;

    public function __construct(RateLimiter $limiter)
    {
        $this->limiter = $limiter;
    }

    public function handle(Request $request, Closure $next, $maxAttempts = 60, $decayMinutes = 1): Response
    {
        $key = $this->resolveRequestSignature($request);

        if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
            Log::warning('API rate limit exceeded', [
                'ip' => $request->ip(),
                'endpoint' => $request->path(),
            ]);

            return response()->json([
                'error' => 'Too Many Attempts.',
                'retry_after' => $this->limiter->availableIn($key),
            ], 429);
        }

        $this->limiter->hit($key, $decayMinutes * 60);

        $response = $next($request);

        // Add rate limit headers to response
        return $this->addHeaders(
            $response,
            $maxAttempts,
            $this->calculateRemainingAttempts($key, $maxAttempts)
        );
    }

    protected function resolveRequestSignature($request)
    {
        return sha1(
            $request->method() .
            '|' . $request->server('SERVER_NAME') .
            '|' . $request->path() .
            '|' . $request->ip()
        );
    }

    protected function addHeaders($response, $maxAttempts, $remainingAttempts)
    {
        $response->headers->add([
            'X-RateLimit-Limit' => $maxAttempts,
            'X-RateLimit-Remaining' => $remainingAttempts,
        ]);

        return $response;
    }

    protected function calculateRemainingAttempts($key, $maxAttempts)
    {
        return $maxAttempts - $this->limiter->attempts($key);
    }
}

Registering Middleware

Step 1: Register in Kernel

<?php
// app/Http/Kernel.php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array<int, class-string|string>
     */
    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        
        // Global custom middleware
        \App\Http\Middleware\LogRequests::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array<string, array<int, class-string|string>>
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            
            // Custom API middleware
            \App\Http\Middleware\ThrottleAPI::class,
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array<string, class-string|string>
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        
        // Custom middleware
        'age.check' => \App\Http\Middleware\CheckUserAge::class,
        'admin' => \App\Http\Middleware\AdminAccess::class,
        'log.requests' => \App\Http\Middleware\LogRequests::class,
        'api.throttle' => \App\Http\Middleware\ThrottleAPI::class,
    ];
}

Applying Middleware to Routes

Route-Specific Middleware

// routes/web.php

use App\Http\Controllers\AdminController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ContentController;

// Single middleware
Route::get('/adult-content', [ContentController::class, 'adult'])
     ->middleware('age.check');

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

// Middleware with parameters
Route::get('/alcohol-content', [ContentController::class, 'alcohol'])
     ->middleware('age.check:21'); // Pass 21 as parameter

// Middleware group
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

// API routes with custom throttling
Route::middleware(['api.throttle:100,1'])->group(function () {
    Route::get('/api/posts', [PostController::class, 'index']);
    Route::post('/api/posts', [PostController::class, 'store']);
});

Controller-Based Middleware

<?php
// app/Http/Controllers/AdminController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class AdminController extends Controller
{
    public function __construct()
    {
        // Apply to all methods
        $this->middleware('auth');
        
        // Apply to specific methods
        $this->middleware('admin')->only(['dashboard', 'users']);
        $this->middleware('log.requests')->only('dashboard');
        
        // Apply to all except specified methods
        $this->middleware('verified')->except(['settings']);
        
        // Middleware with parameters
        $this->middleware('age.check:21')->only('alcoholContent');
    }
    
    public function dashboard()
    {
        return view('admin.dashboard');
    }
    
    public function users()
    {
        return view('admin.users');
    }
    
    public function alcoholContent()
    {
        return view('content.alcohol');
    }
    
    public function settings()
    {
        return view('admin.settings');
    }
}

Advanced Middleware Techniques

Terminable Middleware

<?php
// app/Http/Middleware/PerformanceMonitor.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Log;

class PerformanceMonitor
{
    protected $startTime;

    public function handle(Request $request, Closure $next): Response
    {
        $this->startTime = microtime(true);
        
        return $next($request);
    }

    public function terminate($request, $response): void
    {
        $endTime = microtime(true);
        $executionTime = round(($endTime - $this->startTime) * 1000, 2); // Convert to milliseconds

        // Log slow requests
        if ($executionTime > 500) { // More than 500ms
            Log::warning('Slow Request Detected', [
                'url' => $request->fullUrl(),
                'method' => $request->method(),
                'execution_time' => $executionTime . 'ms',
                'memory_usage' => round(memory_get_peak_usage(true) / 1024 / 1024, 2) . 'MB',
            ]);
        }

        // Log all requests for analytics (in production, consider queuing this)
        Log::channel('performance')->info('Request Performance', [
            'method' => $request->method(),
            'url' => $request->path(),
            'execution_time' => $executionTime,
            'status_code' => $response->getStatusCode(),
            'user_agent' => $request->userAgent(),
        ]);
    }
}

Conditional Middleware

<?php
// app/Http/Middleware/FeatureFlag.php

namespace App\Http\Middleware;

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

class FeatureFlag
{
    public function handle(Request $request, Closure $next, $feature): Response
    {
        if (!$this->isFeatureEnabled($feature)) {
            if ($request->expectsJson()) {
                return response()->json([
                    'error' => 'This feature is currently unavailable.'
                ], 503);
            }

            abort(503, 'This feature is currently unavailable.');
        }

        return $next($request);
    }

    protected function isFeatureEnabled($feature): bool
    {
        $features = [
            'new_design' => env('FEATURE_NEW_DESIGN', false),
            'beta_features' => env('FEATURE_BETA', false),
            'payment_v2' => env('FEATURE_PAYMENT_V2', true),
        ];

        return $features[$feature] ?? false;
    }
}

Localization Middleware

<?php
// app/Http/Middleware/Localization.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Session;

class Localization
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): Response
    {
        // Check URL parameter first (e.g., /en/home)
        if ($request->has('lang')) {
            $locale = $request->get('lang');
            Session::put('locale', $locale);
        }
        // Check session
        elseif (Session::has('locale')) {
            $locale = Session::get('locale');
        }
        // Check browser language
        else {
            $locale = $request->getPreferredLanguage(['en', 'es', 'fr', 'de']);
        }

        // Validate and set locale
        if (in_array($locale, ['en', 'es', 'fr', 'de'])) {
            App::setLocale($locale);
        } else {
            App::setLocale('en'); // Default fallback
        }

        return $next($request);
    }
}

Security Middleware Examples

CSRF Protection for Specific Routes

<?php
// app/Http/Middleware/VerifyCsrfToken.php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array<int, string>
     */
    protected $except = [
        'stripe/*',
        'webhook/*',
        'api/*', // If you're building API that doesn't need CSRF
    ];
}

CORS Middleware for APIs

<?php
// app/Http/Middleware/Cors.php

namespace App\Http\Middleware;

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

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

        $response->headers->set('Access-Control-Allow-Origin', '*');
        $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
        $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
        $response->headers->set('Access-Control-Max-Age', '86400'); // 24 hours

        // Handle preflight requests
        if ($request->getMethod() === 'OPTIONS') {
            $response->setStatusCode(200);
        }

        return $response;
    }
}

XSS Protection Middleware

<?php
// app/Http/Middleware/XSSProtection.php

namespace App\Http\Middleware;

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

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

        // Add security headers
        $response->headers->set('X-XSS-Protection', '1; mode=block');
        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

        return $response;
    }
}

Testing Middleware

Middleware Unit Tests

<?php
// tests/Unit/Middleware/AdminAccessTest.php

namespace Tests\Unit\Middleware;

use Tests\TestCase;
use App\Http\Middleware\AdminAccess;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class AdminAccessTest extends TestCase
{
    public function test_redirects_guests_to_login()
    {
        // Create a request
        $request = Request::create('/admin', 'GET');
        
        // Create middleware instance
        $middleware = new AdminAccess();
        
        // Get response
        $response = $middleware->handle($request, function () {});
        
        // Assert redirection to login
        $this->assertEquals(302, $response->getStatusCode());
        $this->assertStringContainsString('login', $response->getTargetUrl());
    }

    public function test_allows_admin_users()
    {
        // Create admin user
        $user = User::factory()->admin()->create();
        
        // Authenticate user
        Auth::login($user);
        
        $request = Request::create('/admin', 'GET');
        $middleware = new AdminAccess();
        
        $called = false;
        $response = $middleware->handle($request, function () use (&$called) {
            $called = true;
            return response('OK');
        });
        
        $this->assertTrue($called);
        $this->assertEquals('OK', $response->getContent());
    }

    public function test_denies_non_admin_users()
    {
        $user = User::factory()->create(); // Regular user
        Auth::login($user);
        
        $request = Request::create('/admin', 'GET');
        $middleware = new AdminAccess();
        
        $response = $middleware->handle($request, function () {});
        
        $this->assertEquals(302, $response->getStatusCode());
        $this->assertStringContainsString('home', $response->getTargetUrl());
    }

    public function test_returns_json_for_api_requests()
    {
        $user = User::factory()->create();
        Auth::login($user);
        
        $request = Request::create('/admin', 'GET');
        $request->headers->set('Accept', 'application/json');
        
        $middleware = new AdminAccess();
        $response = $middleware->handle($request, function () {});
        
        $this->assertEquals(403, $response->getStatusCode());
        $this->assertJson($response->getContent());
        $this->assertStringContainsString('Unauthorized', $response->getContent());
    }
}

Real-World Middleware Implementation

Complete Authentication System

<?php
// app/Http/Middleware/RedirectIfAuthenticated.php

namespace App\Http\Middleware;

use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class RedirectIfAuthenticated
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, string ...$guards): Response
    {
        $guards = empty($guards) ? [null] : $guards;

        foreach ($guards as $guard) {
            if (Auth::guard($guard)->check()) {
                $user = Auth::guard($guard)->user();
                
                // Redirect based on user role
                if ($user->is_admin) {
                    return redirect()->route('admin.dashboard');
                }
                
                if ($user->hasRole('vendor')) {
                    return redirect()->route('vendor.dashboard');
                }
                
                return redirect(RouteServiceProvider::HOME);
            }
        }

        return $next($request);
    }
}

Maintenance Mode Middleware

<?php
// app/Http/Middleware/CheckForMaintenanceMode.php

namespace App\Http\Middleware;

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

class CheckForMaintenanceMode
{
    public function handle(Request $request, Closure $next): Response
    {
        // Check if maintenance mode is enabled
        if (app()->isDownForMaintenance()) {
            // Allow access for admins even during maintenance
            if ($request->user() && $request->user()->is_admin) {
                return $next($request);
            }
            
            // Allow access to specific IPs (your team)
            $allowedIPs = ['192.168.1.100', '10.0.0.50'];
            if (in_array($request->ip(), $allowedIPs)) {
                return $next($request);
            }
            
            // Show maintenance page for everyone else
            return response()->view('maintenance', [], 503);
        }

        return $next($request);
    }
}

Common Interview Questions & Answers

1. What is Laravel middleware and how does it work?

Middleware are filters that process HTTP requests before they reach your application logic. They can perform tasks like authentication, logging, CORS handling, and more. Requests pass through middleware stacks in order, and responses pass back through them in reverse order.

2. What's the difference between global middleware and route middleware?

Global middleware runs on every request to your application, while route middleware only runs on specific routes where they're assigned. Global middleware is good for tasks like logging or CORS, while route middleware is perfect for authentication or authorization.

3. How do you pass parameters to middleware?

Pass parameters after the middleware name separated by colons: ->middleware('role:admin,editor'). In the middleware, access them as additional parameters in the handle method: public function handle($request, $next, ...$roles).

4. What is terminable middleware?

Terminable middleware runs after the response has been sent to the browser. It's useful for tasks that don't need to block the response, like logging or cleanup operations that can happen after the user receives their response.

5. How do you handle CORS in Laravel?

Laravel has built-in CORS support via the cors middleware. You can configure allowed origins, methods, and headers in the config/cors.php file. For custom requirements, you can create your own CORS middleware.

6. What's the purpose of the $middlewareGroups property in Kernel?

$middlewareGroups allows you to group multiple middleware together under a single name, like 'web' or 'api'. This makes it easier to apply common middleware stacks to routes without listing each middleware individually.

Best Practices

  • Keep middleware focused on a single responsibility
  • Use meaningful names that describe what the middleware does
  • Handle both web and API responses appropriately
  • Use dependency injection in middleware constructors
  • Test your middleware thoroughly
  • Consider performance - avoid heavy operations in global middleware
  • Use middleware groups for common patterns
  • Document custom middleware for your team

Common Middleware Patterns

// Authentication pattern
Route::middleware(['auth', 'verified'])->group(function () {
    // Protected routes
});

// API pattern  
Route::middleware(['auth:sanctum', 'api.throttle'])->group(function () {
    // API routes
});

// Admin pattern
Route::middleware(['auth', 'admin'])->prefix('admin')->group(function () {
    // Admin routes
});

// Localization pattern
Route::middleware(['localization'])->group(function () {
    // Localized routes
});

You've now mastered Laravel Middleware and can effectively filter and process HTTP requests in your applications! In our next post, we'll explore Laravel Form Validation: Ensuring Data Integrity with Simple Rules to learn how to validate user input effectively.