Laravel middleware is one of the most powerful features in the framework, acting as a bridge between incoming HTTP requests and your application's core logic. Whether you're building a simple blog or a complex enterprise application, understanding middleware is essential for creating secure, maintainable, and efficient Laravel applications.
In this comprehensive guide, we'll explore everything you need to know about Laravel 12 middleware, from basic concepts to advanced implementations and best practices.
What is Middleware in Laravel?
Middleware provides a convenient mechanism for inspecting and filtering HTTP requests entering your application. Think of middleware as a series of layers or checkpoints that HTTP requests must pass through before reaching your application's core functionality.
For example, Laravel includes built-in middleware that verifies whether a user is authenticated. If the user isn't authenticated, the middleware redirects them to the login screen. However, if the user is authenticated, the middleware allows the request to proceed deeper into the application.
Middleware can be used for various purposes beyond authentication:
- Request logging: Track all incoming requests for debugging and analytics
- CSRF protection: Ensure requests are legitimate and secure
- Data validation: Validate incoming data before it reaches controllers
- Rate limiting: Prevent abuse by limiting request frequency
- CORS handling: Manage cross-origin resource sharing policies
- API token verification: Authenticate API requests
- Role-based access control: Restrict access based on user roles
- Request/response modification: Transform data before processing or sending
How Middleware Works: The Request Lifecycle
Understanding how middleware fits into Laravel's request lifecycle is crucial. When an HTTP request enters your Laravel application, it follows this path:
- The request enters through the
public/index.phpfile - Laravel bootstraps the application and loads the service providers
- The request passes through the global middleware stack
- Laravel's router matches the request to a route
- Route-specific middleware executes
- The request reaches your controller or route handler
- The response travels back through the middleware layers
- Finally, the response is sent to the client
This pipeline architecture allows each middleware layer to examine, modify, or even reject requests entirely before they reach your application logic.
Creating Custom Middleware in Laravel 12
Creating middleware in Laravel 12 is straightforward using the Artisan command-line tool. Let's walk through the process of creating a custom middleware.
Step 1: Generate Middleware
Use the make:middleware Artisan command to create a new middleware class:
php artisan make:middleware EnsureTokenIsValid
This command creates a new file in the app/Http/Middleware directory with the basic middleware structure already in place.
Step 2: Implement Middleware Logic
Open the generated EnsureTokenIsValid.php file. You'll see a basic template with a handle method:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureTokenIsValid
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->input('token') !== 'my-secret-token') {
return redirect('/home')->with('error', 'Invalid token provided');
}
return $next($request);
}
}
The handle method receives two parameters:
$request: The incoming HTTP request object$next: A closure that represents the next middleware in the stack
To allow the request to proceed, call $next($request). To reject the request, return a response or redirect.
Step 3: Before and After Middleware
Middleware can perform tasks before or after the request is handled by the application.
Before Middleware (executed before the request reaches the controller):
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class BeforeMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// Perform action before the request is handled
Log::info('Request received: ' . $request->path());
return $next($request);
}
}
After Middleware (executed after the request is handled):
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class AfterMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// Perform action after the request is handled
Log::info('Response sent: ' . $response->getStatusCode());
return $response;
}
}
Registering Middleware in Laravel 12
One of the significant changes in Laravel 12 is how middleware is registered. Instead of the traditional app/Http/Kernel.php file, middleware registration now occurs in the bootstrap/app.php file. This change centralizes configuration and makes the setup process more intuitive.
The New Bootstrap/App.php Structure
Here's the basic structure of bootstrap/app.php in Laravel 12:
<?php
use Illuminate\Foundation\Application;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function ($middleware) {
// Register middleware here
})
->withExceptions(function ($exceptions) {
//
})
->create();
Global Middleware
Global middleware runs on every HTTP request to your application. To register global middleware, use the append or prepend methods within the withMiddleware closure:
->withMiddleware(function ($middleware) {
$middleware->append(\App\Http\Middleware\EnsureTokenIsValid::class);
})
Use append to add middleware to the end of the global stack, or prepend to add it to the beginning:
->withMiddleware(function ($middleware) {
$middleware->prepend(\App\Http\Middleware\LogRequests::class);
$middleware->append(\App\Http\Middleware\CompressResponse::class);
})
Route Middleware
Route middleware is assigned to specific routes or route groups. To register route middleware (also called middleware aliases), use the alias method:
->withMiddleware(function ($middleware) {
$middleware->alias([
'admin' => \App\Http\Middleware\CheckAdmin::class,
'verified.email' => \App\Http\Middleware\EnsureEmailIsVerified::class,
'check.token' => \App\Http\Middleware\EnsureTokenIsValid::class,
]);
})
Middleware Groups
Middleware groups allow you to bundle multiple middleware under a single key, making it easier to apply them to routes:
->withMiddleware(function ($middleware) {
$middleware->appendToGroup('api', [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
]);
})
Laravel 12 comes with pre-configured middleware groups like web and api. You can add your custom middleware to these groups or create new groups entirely.
Applying Middleware to Routes
Once middleware is registered, you can apply it to routes in several ways.
Single Route Middleware
Apply middleware to individual routes using the middleware method:
use Illuminate\Support\Facades\Route;
Route::get('/dashboard', function () {
// Dashboard logic
})->middleware('auth');
Multiple Middleware on a Route
Chain multiple middleware by passing an array:
Route::get('/admin/settings', function () {
// Admin settings
})->middleware(['auth', 'admin', 'verified.email']);
Route Group Middleware
Apply middleware to a group of routes:
Route::middleware(['auth', 'verified.email'])->group(function () {
Route::get('/profile', [ProfileController::class, 'show']);
Route::put('/profile', [ProfileController::class, 'update']);
Route::delete('/profile', [ProfileController::class, 'destroy']);
});
Controller Middleware
Apply middleware directly in your controller's constructor:
<?php
namespace App\Http\Controllers;
class AdminController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('admin')->only(['destroy', 'create']);
$this->middleware('log.request')->except(['index']);
}
}
Middleware Parameters
Middleware can accept additional parameters, making them more flexible and reusable. This is particularly useful for role-based access control or feature flags.
Creating Parameterized Middleware
Here's an example of middleware that checks user roles:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckRole
{
public function handle(Request $request, Closure $next, string $role): Response
{
if (! $request->user() || ! $request->user()->hasRole($role)) {
abort(403, 'Unauthorized action.');
}
return $next($request);
}
}
Using Parameterized Middleware
Pass parameters to middleware using a colon separator:
Route::get('/admin/dashboard', function () {
// Admin only
})->middleware('role:admin');
Route::get('/moderator/panel', function () {
// Moderator only
})->middleware('role:moderator');
You can pass multiple parameters separated by commas:
Route::get('/content/edit', function () {
// Edit content
})->middleware('role:admin,editor');
Your middleware would receive these as separate parameters:
public function handle(Request $request, Closure $next, string ...$roles): Response
{
if (! $request->user() || ! $request->user()->hasAnyRole($roles)) {
abort(403, 'Unauthorized action.');
}
return $next($request);
}
Dependency Injection in Middleware
Laravel's service container resolves all middleware, allowing you to type-hint any dependencies you need in the middleware constructor:
<?php
namespace App\Http\Middleware;
use App\Services\TokenService;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ValidateApiToken
{
protected $tokenService;
public function __construct(TokenService $tokenService)
{
$this->tokenService = $tokenService;
}
public function handle(Request $request, Closure $next): Response
{
$token = $request->header('X-API-Token');
if (! $this->tokenService->isValid($token)) {
return response()->json(['error' => 'Invalid API token'], 401);
}
return $next($request);
}
}
Terminable Middleware
Sometimes you need to perform work after the HTTP response has been sent to the browser. Terminable middleware allows you to do this by implementing a terminate method:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class TerminableMiddleware
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
// Perform tasks after response is sent
// This won't delay the response to the user
Log::info('Response completed', [
'status' => $response->getStatusCode(),
'path' => $request->path(),
]);
}
}
The terminate method is called after the response has been sent to the user, making it ideal for tasks like logging, data syncing, or cleanup operations that don't need to block the response.
Real-World Middleware Examples
Let's explore some practical middleware implementations you might use in production applications.
API Rate Limiting Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;
class RateLimitApi
{
public function handle(Request $request, Closure $next, int $maxAttempts = 60): Response
{
$key = 'rate-limit:' . $request->ip();
$attempts = Cache::get($key, 0);
if ($attempts >= $maxAttempts) {
return response()->json([
'error' => 'Too many requests. Please try again later.'
], 429);
}
Cache::put($key, $attempts + 1, now()->addMinute());
$response = $next($request);
$response->headers->set('X-RateLimit-Limit', $maxAttempts);
$response->headers->set('X-RateLimit-Remaining', $maxAttempts - $attempts - 1);
return $response;
}
}
Request Logging Middleware
<?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);
$response = $next($request);
$duration = microtime(true) - $startTime;
Log::info('HTTP Request', [
'method' => $request->method(),
'url' => $request->fullUrl(),
'ip' => $request->ip(),
'user_id' => $request->user()?->id,
'status' => $response->getStatusCode(),
'duration' => round($duration * 1000, 2) . 'ms',
]);
return $response;
}
}
Force HTTPS Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ForceHttps
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri(), 301);
}
return $next($request);
}
}
Sanitize Input Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SanitizeInput
{
public function handle(Request $request, Closure $next): Response
{
$input = $request->all();
array_walk_recursive($input, function (&$value) {
if (is_string($value)) {
$value = strip_tags($value);
$value = trim($value);
}
});
$request->merge($input);
return $next($request);
}
}
Localization Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = $request->segment(1);
$availableLocales = ['en', 'es', 'fr', 'de'];
if (in_array($locale, $availableLocales)) {
App::setLocale($locale);
}
return $next($request);
}
}
Built-in Laravel Middleware
Laravel 12 includes several powerful built-in middleware classes that handle common tasks:
Authentication Middleware
Authenticate: Ensures the user is logged inRedirectIfAuthenticated: Redirects authenticated users away from guest-only pages
CSRF Protection
VerifyCsrfToken: Protects against cross-site request forgery attacks on POST, PUT, PATCH, and DELETE requests
Session Management
StartSession: Handles session dataShareErrorsFromSession: Makes validation errors available in views
Cookie Encryption
EncryptCookies: Automatically encrypts and decrypts cookiesAddQueuedCookiesToResponse: Adds cookies to the response
Security Headers
HandleCors: Manages Cross-Origin Resource Sharing (CORS) policiesTrustProxies: Configures trusted proxies for applications behind load balancers
Maintenance Mode
PreventRequestsDuringMaintenance: Blocks requests when the application is in maintenance mode
Middleware Best Practices
Following these best practices will help you create maintainable, secure, and performant middleware:
Keep Middleware Simple and Focused
Each middleware should have a single responsibility. If your middleware is doing too much, consider splitting it into multiple middleware classes:
// Bad: Middleware doing too many things
class HandleRequestMiddleware
{
public function handle($request, $next)
{
// Authenticate
// Validate
// Log
// Transform data
// etc...
}
}
// Good: Separate concerns
class AuthenticateMiddleware { }
class ValidateRequestMiddleware { }
class LogRequestMiddleware { }
class TransformRequestMiddleware { }
Use Middleware Parameters for Flexibility
Make middleware reusable by accepting parameters:
// Instead of creating CheckAdminMiddleware, CheckModeratorMiddleware, etc.
// Create one flexible middleware
class CheckRole
{
public function handle($request, $next, ...$roles)
{
if (! $request->user()->hasAnyRole($roles)) {
abort(403);
}
return $next($request);
}
}
// Usage
Route::get('/admin', fn() => '...')->middleware('role:admin');
Route::get('/content', fn() => '...')->middleware('role:admin,editor');
Order Middleware Appropriately
The order of middleware execution matters. Place authentication middleware before middleware that requires user data:
Route::middleware(['auth', 'verified', 'role:admin'])->group(function () {
// Routes here
});
Avoid Middleware Overuse
Not every piece of logic belongs in middleware. Use middleware for cross-cutting concerns that apply to multiple routes. For route-specific logic, consider using:
- Controller methods
- Form request validation classes
- Service classes
- Route model binding
Optimize Middleware Performance
Keep middleware logic lightweight. For expensive operations, consider:
- Using caching to avoid repeated calculations
- Moving heavy processing to queued jobs
- Implementing terminable middleware for post-response tasks
public function handle($request, $next)
{
// Fast checks only
if (Cache::has('user-banned:' . $request->user()->id)) {
abort(403);
}
return $next($request);
}
public function terminate($request, $response)
{
// Heavy processing after response
SomeHeavyJob::dispatch($request->user());
}
Handle Exceptions Gracefully
Always handle potential exceptions in middleware to prevent application crashes:
public function handle($request, $next)
{
try {
// Validate token
$token = $request->header('X-API-Token');
$this->tokenService->validate($token);
} catch (InvalidTokenException $e) {
return response()->json(['error' => 'Invalid token'], 401);
}
return $next($request);
}
Test Your Middleware
Write comprehensive tests for your middleware to ensure they work as expected:
<?php
namespace Tests\Feature;
use Tests\TestCase;
class CheckAdminMiddlewareTest extends TestCase
{
public function test_non_admin_cannot_access_admin_routes()
{
$user = User::factory()->create(['role' => 'user']);
$response = $this->actingAs($user)->get('/admin/dashboard');
$response->assertStatus(403);
}
public function test_admin_can_access_admin_routes()
{
$admin = User::factory()->create(['role' => 'admin']);
$response = $this->actingAs($admin)->get('/admin/dashboard');
$response->assertStatus(200);
}
}
Use Type Hints and Return Types
Leverage PHP's type system for better code quality and IDE support:
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
public function handle(Request $request, Closure $next): Response
{
// Implementation
}
Document Middleware Parameters
When middleware accepts parameters, document them clearly:
/**
* Check if the user has the required role.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string ...$roles Required roles (admin, editor, moderator)
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next, string ...$roles): Response
{
// Implementation
}
Security Considerations
Middleware plays a crucial role in application security. Follow these guidelines:
Always Use CSRF Protection
Laravel's VerifyCsrfToken middleware should be included in your web middleware group for all state-changing requests:
->withMiddleware(function ($middleware) {
$middleware->appendToGroup('web', [
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
]);
})
Validate API Tokens Properly
For API routes, implement proper token validation:
public function handle($request, $next)
{
$token = $request->bearerToken();
if (! $token || ! $this->isValidToken($token)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $next($request);
}
Implement Rate Limiting
Protect your application from abuse by implementing rate limiting, especially for API endpoints and authentication routes:
Route::middleware(['throttle:60,1'])->group(function () {
// Limited to 60 requests per minute
});
Sanitize User Input
While Laravel provides CSRF protection and SQL injection prevention, additional input sanitization in middleware can add an extra security layer:
public function handle($request, $next)
{
$input = $request->all();
array_walk_recursive($input, function (&$value) {
if (is_string($value)) {
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
});
$request->merge($input);
return $next($request);
}
Protect Sensitive Routes
Use middleware to ensure sensitive routes are properly protected:
Route::middleware(['auth', 'verified', '2fa', 'role:admin'])->group(function () {
// Highly sensitive admin routes
});
Debugging Middleware
When middleware doesn't work as expected, use these debugging techniques:
Enable Query Logging
public function handle($request, $next)
{
DB::enableQueryLog();
$response = $next($request);
Log::debug('Queries executed:', DB::getQueryLog());
return $response;
}
Log Middleware Execution
public function handle($request, $next)
{
Log::debug('Middleware executed: ' . static::class);
return $next($request);
}
Use Laravel Telescope
Laravel Telescope provides excellent insights into middleware execution, request handling, and performance metrics. Install it for development:
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
Middleware vs. Other Laravel Features
Understanding when to use middleware versus other Laravel features is important:
Middleware vs. Form Requests
- Use Middleware: For cross-cutting concerns that apply to multiple routes
- Use Form Requests: For route-specific validation logic
Middleware vs. Gates and Policies
- Use Middleware: For route-level authorization
- Use Gates/Policies: For resource-level authorization within controllers
Middleware vs. Service Classes
- Use Middleware: For HTTP request/response manipulation
- Use Service Classes: For business logic that isn't tied to HTTP
Middleware vs. Events and Listeners
- Use Middleware: For synchronous request/response handling
- Use Events/Listeners: For decoupled, potentially asynchronous operations
Performance Optimization
Optimize middleware performance with these strategies:
Cache Expensive Operations
public function handle($request, $next)
{
$userId = $request->user()->id;
$permissions = Cache::remember(
"user-permissions:{$userId}",
3600,
fn() => $this->permissionService->getUserPermissions($userId)
);
$request->merge(['permissions' => $permissions]);
return $next($request);
}
Use Early Returns
Exit middleware as early as possible when conditions aren't met:
public function handle($request, $next)
{
if (! $request->user()) {
return redirect('/login');
}
if (! $request->user()->isActive()) {
return response('Account suspended', 403);
}
return $next($request);
}
Lazy Load Dependencies
Use dependency injection wisely and avoid loading unnecessary services:
public function handle($request, $next)
{
// Only load service if needed
if ($request->has('validate')) {
app(ValidationService::class)->validate($request);
}
return $next($request);
}
Common Middleware Pitfalls to Avoid
Avoid these common mistakes when working with middleware:
Not Returning the Response
Always return the result of $next($request):
// Wrong
public function handle($request, $next)
{
if (! $request->user()) {
redirect('/login'); // Missing return!
}
$next($request); // Missing return!
}
// Correct
public function handle($request, $next)
{
if (! $request->user()) {
return redirect('/login');
}
return $next($request);
}
Modifying Request After Calling $next
Modifications to the request after calling $next() won't affect the application:
// Wrong
public function handle($request, $next)
{
$response = $next($request);
$request->merge(['foo' => 'bar']); // Too late!
return $response;
}
// Correct
public function handle($request, $next)
{
$request->merge(['foo' => 'bar']);
return $next($request);
}
Forgetting to Register Middleware
Always register your middleware in bootstrap/app.php before using it in routes.
Incorrect Middleware Order
Pay attention to middleware order. Authentication should come before authorization:
// Wrong order
Route::middleware(['role:admin', 'auth'])
// Correct order
Route::middleware(['auth', 'role:admin'])
Migrating from Laravel 11 to Laravel 12 Middleware
If you're upgrading from Laravel 11, here's how to migrate your middleware configuration:
Old Way (Laravel 11 - app/Http/Kernel.php)
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
];
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
],
];
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
];
New Way (Laravel 12 - bootstrap/app.php)
->withMiddleware(function ($middleware) {
// Global middleware
$middleware->append(\App\Http\Middleware\TrustProxies::class);
// Middleware groups
$middleware->appendToGroup('web', [
\App\Http\Middleware\EncryptCookies::class,
]);
// Route middleware aliases
$middleware->alias([
'auth' => \App\Http\Middleware\Authenticate::class,
]);
})
Conclusion
Laravel middleware is a powerful feature that provides clean, reusable solutions for handling cross-cutting concerns in your application. By understanding how middleware works and following best practices, you can build more secure, maintainable, and performant Laravel applications.
Key takeaways:
- Middleware acts as a filter for HTTP requests entering your application
- Laravel 12 moved middleware registration from
Kernel.phptobootstrap/app.php - Different types of middleware serve different purposes: global, route, and group middleware
- Middleware can accept parameters for increased flexibility
- Keep middleware simple, focused, and well-tested
- Use middleware for security, logging, request transformation, and cross-cutting concerns
- Follow best practices for performance, security, and maintainability
As you continue working with Laravel, you'll discover more use cases for middleware and develop your own patterns for implementing them effectively. The key is to keep your middleware focused, well-organized, and properly documented.
Happy coding!