Request Middleware Controller
Backend

Laravel Middleware: A Complete Guide

Mayur Dabhi
Mayur Dabhi
March 10, 2026
18 min read

Middleware is one of the most powerful features in Laravel, acting as a filtering mechanism for HTTP requests entering your application. Whether you need to authenticate users, log requests, modify responses, or implement custom security measures, middleware provides an elegant, reusable way to handle these cross-cutting concerns.

In this comprehensive guide, we'll explore everything about Laravel middleware—from understanding the basics to implementing advanced patterns that will make your applications more secure, maintainable, and efficient.

Why Master Middleware?

Middleware is the gatekeeping layer of your Laravel application. Understanding how to create and use middleware effectively separates beginner developers from those who build robust, production-ready applications.

Understanding the Middleware Pipeline

Before diving into code, let's visualize how middleware works in Laravel. Think of middleware as a series of "layers" that wrap around your application. Each HTTP request must pass through these layers before reaching your controller, and the response passes back through them on the way out.

Laravel Middleware Pipeline (Onion Model) Global Middleware Layer Route Middleware Layer Controller Middleware Your Application HTTP Request HTTP Response Runs on ALL requests Runs on specific routes Runs on controller actions

Middleware wraps your application like layers of an onion—requests pass inward, responses pass outward

Creating Your First Middleware

Laravel makes creating middleware incredibly simple with the Artisan command:

Terminal
php artisan make:middleware CheckAge

This creates a new middleware class in app/Http/Middleware. Let's look at the basic structure:

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

namespace App\Http\Middleware;

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

class CheckAge
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->age <= 18) {
            return redirect('home')->with('error', 'You must be over 18.');
        }

        return $next($request);
    }
}

The $next closure is crucial—it passes the request to the next middleware in the pipeline. If you don't call $next($request), the request stops at your middleware and never reaches the controller.

Before & After Middleware

Middleware can perform actions before OR after the request is handled. Here's the difference:

Executes logic BEFORE the request reaches the controller:

public function handle(Request $request, Closure $next): Response
{
    // This runs BEFORE the request reaches the controller
    if (!$this->checkUserPermission($request)) {
        abort(403, 'Unauthorized');
    }

    return $next($request); // Pass to next layer
}

Executes logic AFTER the response is generated:

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

    // This runs AFTER the controller generates response
    $response->header('X-Custom-Header', 'MyValue');
    
    return $response;
}

Perform actions both before and after:

public function handle(Request $request, Closure $next): Response
{
    // BEFORE: Start timing
    $startTime = microtime(true);
    
    // Process request through pipeline
    $response = $next($request);
    
    // AFTER: Log duration
    $duration = microtime(true) - $startTime;
    Log::info("Request took {$duration}ms");
    
    return $response;
}

Registering Middleware

After creating middleware, you need to register it. Laravel 11 simplified this process significantly. Here's how to register middleware:

bootstrap/app.php (Laravel 11+)
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\CheckAge;
use App\Http\Middleware\LogRequests;
use App\Http\Middleware\AdminOnly;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // Global middleware (runs on every request)
        $middleware->append(LogRequests::class);
        
        // Aliased middleware (use by name in routes)
        $middleware->alias([
            'age' => CheckAge::class,
            'admin' => AdminOnly::class,
        ]);
        
        // Middleware groups
        $middleware->appendToGroup('web', [
            // Additional web middleware
        ]);
        
        $middleware->appendToGroup('api', [
            // Additional API middleware
        ]);
    })
    ->create();
Laravel 10 and Earlier

In Laravel 10 and earlier versions, middleware is registered in app/Http/Kernel.php using the $middleware, $middlewareGroups, and $middlewareAliases arrays.

Middleware Types Overview

Type Scope Use Case
Global Every HTTP request Logging, CORS, Security headers
Route Specific routes Authentication, Rate limiting
Group Route groups API vs Web specific logic
Controller Controller methods Action-specific checks

Applying Middleware to Routes

Once registered, you can apply middleware to routes in several ways:

routes/web.php
<?php

use App\Http\Controllers\ProfileController;
use App\Http\Middleware\CheckAge;
use App\Http\Middleware\VerifyApiToken;

// Single middleware using alias
Route::get('/adult-content', function () {
    return view('adult.content');
})->middleware('age');

// Multiple middleware
Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware(['auth', 'verified', 'age']);

// Using middleware class directly
Route::get('/profile', [ProfileController::class, 'show'])
    ->middleware(CheckAge::class);

// Route groups with middleware
Route::middleware(['auth', 'admin'])->group(function () {
    Route::get('/admin', [AdminController::class, 'index']);
    Route::get('/admin/users', [AdminController::class, 'users']);
    Route::get('/admin/settings', [AdminController::class, 'settings']);
});

// Middleware with parameters
Route::get('/posts', [PostController::class, 'index'])
    ->middleware('role:editor,admin');

// Excluding middleware
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    
    // Exclude auth from this route
    Route::get('/public-page', [PageController::class, 'public'])
        ->withoutMiddleware('auth');
});

Middleware Parameters

Middleware can accept parameters, making them incredibly flexible. This is perfect for role-based access control:

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

namespace App\Http\Middleware;

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

class CheckRole
{
    /**
     * Handle an incoming request.
     *
     * @param  string  ...$roles  Allowed roles
     */
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        $user = $request->user();
        
        if (!$user) {
            return redirect('login');
        }
        
        // Check if user has any of the allowed roles
        foreach ($roles as $role) {
            if ($user->hasRole($role)) {
                return $next($request);
            }
        }
        
        abort(403, 'You do not have the required role to access this resource.');
    }
}

Use it in your routes with parameters separated by commas:

routes/web.php
// Register alias in bootstrap/app.php first:
// 'role' => CheckRole::class

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

// Multiple roles (any of these can access)
Route::get('/reports', [ReportController::class, 'index'])
    ->middleware('role:admin,manager,analyst');

// Combined with other middleware
Route::get('/billing', [BillingController::class, 'index'])
    ->middleware(['auth', 'verified', 'role:admin,accountant']);

Common Middleware Patterns

Let's explore some real-world middleware implementations you'll commonly need:

API Token Auth

Validate bearer tokens for API requests

Rate Limiting

Prevent abuse by limiting requests

CORS

Handle cross-origin requests

Maintenance Mode

Bypass for specific users/IPs

1. API Authentication Middleware

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

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use App\Models\ApiToken;
use Symfony\Component\HttpFoundation\Response;

class VerifyApiToken
{
    public function handle(Request $request, Closure $next): Response
    {
        $token = $request->bearerToken();
        
        if (!$token) {
            return response()->json([
                'error' => 'No API token provided',
                'code' => 'TOKEN_MISSING'
            ], 401);
        }
        
        $apiToken = ApiToken::where('token', hash('sha256', $token))
            ->where('expires_at', '>', now())
            ->first();
        
        if (!$apiToken) {
            return response()->json([
                'error' => 'Invalid or expired token',
                'code' => 'TOKEN_INVALID'
            ], 401);
        }
        
        // Attach user to request for controller access
        $request->merge(['api_user' => $apiToken->user]);
        
        // Update last used timestamp
        $apiToken->touch('last_used_at');
        
        return $next($request);
    }
}

2. Request Logging Middleware

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

namespace App\Http\Middleware;

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

class LogRequests
{
    public function handle(Request $request, Closure $next): Response
    {
        $startTime = microtime(true);
        
        // Process request
        $response = $next($request);
        
        // Calculate duration
        $duration = round((microtime(true) - $startTime) * 1000, 2);
        
        // Log request details
        Log::channel('requests')->info('HTTP Request', [
            'method' => $request->method(),
            'url' => $request->fullUrl(),
            'ip' => $request->ip(),
            'user_agent' => $request->userAgent(),
            'user_id' => $request->user()?->id,
            'status' => $response->getStatusCode(),
            'duration_ms' => $duration,
        ]);
        
        // Add timing header for debugging
        $response->headers->set('X-Response-Time', $duration . 'ms');
        
        return $response;
    }
}

3. JSON Response Transformer

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

namespace App\Http\Middleware;

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

class TransformApiResponse
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);
        
        // Only transform JSON responses
        if (!$response instanceof JsonResponse) {
            return $response;
        }
        
        $original = $response->getData(true);
        
        // Wrap response in standard format
        $transformed = [
            'success' => $response->isSuccessful(),
            'data' => $original,
            'meta' => [
                'timestamp' => now()->toIso8601String(),
                'version' => config('app.api_version', '1.0'),
            ],
        ];
        
        // Include errors for non-success responses
        if (!$response->isSuccessful()) {
            $transformed['error'] = [
                'code' => $response->getStatusCode(),
                'message' => $original['message'] ?? 'An error occurred',
            ];
            unset($transformed['data']);
        }
        
        return $response->setData($transformed);
    }
}

4. Security Headers Middleware

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

namespace App\Http\Middleware;

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

class SecurityHeaders
{
    protected array $headers = [
        'X-Content-Type-Options' => 'nosniff',
        'X-Frame-Options' => 'SAMEORIGIN',
        'X-XSS-Protection' => '1; mode=block',
        'Referrer-Policy' => 'strict-origin-when-cross-origin',
        'Permissions-Policy' => 'camera=(), microphone=(), geolocation=()',
    ];

    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);
        
        foreach ($this->headers as $header => $value) {
            $response->headers->set($header, $value);
        }
        
        // Add HSTS in production
        if (app()->isProduction()) {
            $response->headers->set(
                'Strict-Transport-Security',
                'max-age=31536000; includeSubDomains'
            );
        }
        
        return $response;
    }
}

Security Best Practice

Apply SecurityHeaders middleware globally to ensure all responses include proper security headers. This protects against clickjacking, XSS, and other common attacks.

Terminable Middleware

Sometimes you need to perform actions after the response has been sent to the browser. Terminable middleware handles this with the terminate method:

Terminable Middleware Lifecycle Request Incoming handle() Process request Controller Generate response Response Sent to client terminate() After response terminate() runs AFTER the response is sent — perfect for logging, cleanup, or analytics
app/Http/Middleware/AnalyticsMiddleware.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use App\Services\AnalyticsService;
use Symfony\Component\HttpFoundation\Response;

class AnalyticsMiddleware
{
    protected $startTime;
    protected $request;

    public function __construct(
        protected AnalyticsService $analytics
    ) {}

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

    /**
     * Handle tasks after the response has been sent to the browser.
     */
    public function terminate(Request $request, Response $response): void
    {
        // This runs AFTER the response is sent to the client
        // Perfect for slow operations that shouldn't delay response
        
        $this->analytics->track([
            'event' => 'page_view',
            'url' => $request->fullUrl(),
            'method' => $request->method(),
            'status' => $response->getStatusCode(),
            'duration' => microtime(true) - $this->startTime,
            'user_id' => $request->user()?->id,
            'ip' => $request->ip(),
            'user_agent' => $request->userAgent(),
            'referrer' => $request->header('referer'),
        ]);
    }
}
When to Use Terminate
  • Sending analytics data to external services
  • Writing to slow log storage systems
  • Cleaning up temporary resources
  • Sending non-critical notifications

Controller Middleware

You can also apply middleware directly within controllers for more granular control:

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

namespace App\Http\Controllers;

use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;

class PostController extends Controller implements HasMiddleware
{
    /**
     * Get the middleware that should be assigned to the controller.
     */
    public static function middleware(): array
    {
        return [
            // Apply to all methods
            'auth',
            
            // Apply only to specific methods
            new Middleware('verified', only: ['store', 'update', 'destroy']),
            
            // Exclude from specific methods
            new Middleware('throttle:60,1', except: ['index', 'show']),
            
            // With parameters
            new Middleware('role:editor,admin', only: ['destroy']),
        ];
    }

    public function index()
    {
        // Anyone can view (only auth required)
        return view('posts.index');
    }

    public function store()
    {
        // Requires auth + verified + throttle
    }

    public function destroy($id)
    {
        // Requires auth + verified + throttle + role:editor,admin
    }
}

Middleware Priority

Sometimes the order in which middleware runs matters. Laravel allows you to define priority:

bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    // Define priority order
    $middleware->priority([
        \App\Http\Middleware\StartSession::class,
        \App\Http\Middleware\AuthenticateSession::class,
        \Illuminate\Auth\Middleware\Authenticate::class,
        \App\Http\Middleware\CheckRole::class,
        \App\Http\Middleware\CheckPermission::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ]);
})
Important

Middleware priority only affects the order when multiple middleware are applied. Session middleware should always run before authentication, and authentication before authorization.

Middleware Groups

Groups let you bundle multiple middleware under a single name. Laravel comes with web and api groups by default:

bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    // Create a custom middleware group
    $middleware->group('admin', [
        'auth',
        'verified',
        \App\Http\Middleware\CheckRole::class.':admin',
        \App\Http\Middleware\LogAdminActions::class,
    ]);
    
    // Append to existing groups
    $middleware->appendToGroup('web', [
        \App\Http\Middleware\LocaleMiddleware::class,
    ]);
    
    $middleware->prependToGroup('api', [
        \App\Http\Middleware\TransformApiResponse::class,
    ]);
})

Now use your custom group in routes:

routes/web.php
// All routes in this group get the 'admin' middleware stack
Route::middleware('admin')->prefix('admin')->group(function () {
    Route::get('/', [AdminController::class, 'dashboard']);
    Route::get('/users', [AdminController::class, 'users']);
    Route::get('/analytics', [AdminController::class, 'analytics']);
});

Testing Middleware

Testing middleware is crucial to ensure your security and validation logic works correctly:

tests/Feature/MiddlewareTest.php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class MiddlewareTest extends TestCase
{
    use RefreshDatabase;

    public function test_guest_cannot_access_protected_route(): void
    {
        $response = $this->get('/dashboard');
        
        $response->assertRedirect('/login');
    }

    public function test_authenticated_user_can_access_protected_route(): void
    {
        $user = User::factory()->create();
        
        $response = $this->actingAs($user)
            ->get('/dashboard');
        
        $response->assertOk();
    }

    public function test_non_admin_cannot_access_admin_routes(): void
    {
        $user = User::factory()->create(['role' => 'user']);
        
        $response = $this->actingAs($user)
            ->get('/admin');
        
        $response->assertForbidden();
    }

    public function test_admin_can_access_admin_routes(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        
        $response = $this->actingAs($admin)
            ->get('/admin');
        
        $response->assertOk();
    }

    public function test_middleware_can_be_disabled_for_testing(): void
    {
        // Disable all middleware for this test
        $response = $this->withoutMiddleware()
            ->get('/protected-route');
        
        $response->assertOk();
    }

    public function test_specific_middleware_can_be_disabled(): void
    {
        $response = $this->withoutMiddleware([
            \App\Http\Middleware\CheckRole::class,
        ])->get('/admin');
        
        // Other middleware still runs, just not CheckRole
    }
}

Best Practices

1

Keep Middleware Focused

Each middleware should do one thing well. Avoid creating "god" middleware that handles authentication, logging, and response transformation all at once.

2

Use Dependency Injection

Inject services through the constructor instead of using facades. This makes your middleware more testable and follows SOLID principles.

3

Always Return a Response

Every path through your middleware must either call $next($request) or return a response. Forgetting this causes hard-to-debug issues.

4

Be Careful with Global Middleware

Global middleware runs on every request. Keep them lightweight and fast. Heavy operations should be in route-specific middleware.

5

Use Terminable Middleware Wisely

The terminate method is great for non-blocking operations, but remember it still runs synchronously. For truly async tasks, dispatch jobs to queues.

Conclusion

Laravel middleware is a powerful mechanism for filtering HTTP requests and responses. By mastering middleware, you can:

The key to effective middleware is keeping each piece focused and composable. Build small, reusable middleware that can be combined in different ways across your routes. This approach leads to maintainable, testable code that's easy to reason about.

Start with the built-in Laravel middleware, understand how they work, and gradually build your own as your application's needs grow. Happy coding!

Laravel Middleware PHP Authentication Security Backend
Mayur Dabhi

Mayur Dabhi

Full-stack developer passionate about Laravel, React, and building clean, efficient web applications. Sharing knowledge to help developers level up their skills.