Master Laravel middleware with advanced patterns including dynamic pipelines, request transformation, response modification, multi-tenant architecture, and layered authentication strategies with production-ready examples.
Laravel’s middleware system is far more powerful than simple authentication guards. It’s a sophisticated request/response pipeline that enables you to transform requests, modify responses, inject context, and build layered architecture patterns. Understanding advanced middleware patterns is essential for building scalable, maintainable Laravel applications.
This comprehensive guide covers everything from basic middleware concepts to advanced patterns including dynamic pipelines, terminating middleware, parameter-based logic, multi-tenant context injection, and real-world implementations.
Understanding Laravel’s Middleware Pipeline
How Middleware Works
Laravel processes HTTP requests through a pipeline of middleware layers, each capable of inspecting, modifying, or rejecting requests before they reach your application logic.
The Pipeline Flow:
Request → Global Middleware → Route Middleware → Controller → Response
↑ ↓
└─────────── Terminating Middleware (optional) ────────────┘
Each middleware can:
- Pre-process the request (before passing to next layer)
- Post-process the response (after receiving from next layer)
- Short-circuit the pipeline (return early without calling next)
- Modify request or response data
- Inject services or context into the application
The Three Middleware Layers
1. Global Middleware (app/Http/Kernel.php):
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
];
Runs on every request to your application. Use sparingly for critical operations.
2. Middleware Groups (app/Http/Kernel.php):
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,
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
Applied to routes based on group assignment (web vs api).
3. Route Middleware (app/Http/Kernel.php):
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
Applied selectively to specific routes or route groups.
Learn how middleware integrates with Laravel Eloquent relationships and custom Artisan commands.
Creating Custom Middleware
Basic Middleware Structure
Generate middleware using Artisan:
php artisan make:middleware CheckSubscriptionStatus
This creates app/Http/Middleware/CheckSubscriptionStatus.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckSubscriptionStatus
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next)
{
// Pre-request logic here
$response = $next($request);
// Post-request logic here
return $response;
}
}
Pre-Request Middleware
Execute logic before the request reaches the controller:
public function handle(Request $request, Closure $next)
{
if (!$request->user()->hasActiveSubscription()) {
return redirect()->route('subscription.renew')
->with('error', 'Your subscription has expired.');
}
return $next($request);
}
Post-Request Middleware
Modify the response after controller execution:
public function handle(Request $request, Closure $next)
{
$response = $next($request);
// Add custom headers to response
$response->headers->set('X-Request-ID', Str::uuid());
$response->headers->set('X-Processed-By', config('app.name'));
return $response;
}
Middleware with Early Exit
Short-circuit the pipeline without calling subsequent middleware:
public function handle(Request $request, Closure $next)
{
if ($this->isMaintenanceWindow()) {
return response()->json([
'error' => 'Service temporarily unavailable',
'retry_after' => $this->maintenanceEndsAt(),
], 503);
}
return $next($request);
}
Dynamic Middleware with Parameters
Passing Parameters to Middleware
Define parameterised middleware in routes:
Route::get('/admin/dashboard', [DashboardController::class, 'index'])
->middleware('role:admin');
Route::get('/editor/posts', [PostController::class, 'index'])
->middleware('role:admin,editor');
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('permission:edit-posts');
Implementing Parameterised Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckRole
{
public function handle(Request $request, Closure $next, ...$roles)
{
if (!$request->user()) {
return redirect()->route('login');
}
foreach ($roles as $role) {
if ($request->user()->hasRole($role)) {
return $next($request);
}
}
abort(403, 'Unauthorized action.');
}
}
Register in app/Http/Kernel.php:
protected $routeMiddleware = [
'role' => \App\Http\Middleware\CheckRole::class,
];
Advanced: Permission-Based Access
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckPermission
{
public function handle(Request $request, Closure $next, string $permission, string $resource = null)
{
$user = $request->user();
if (!$user) {
return redirect()->route('login');
}
// Check direct permission
if ($user->hasPermission($permission)) {
return $next($request);
}
// Check resource-specific permission
if ($resource && $request->route($resource)) {
$model = $request->route($resource);
if ($user->hasPermissionForResource($permission, $model)) {
return $next($request);
}
}
abort(403, "You don't have permission to {$permission}.");
}
}
Usage:
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('permission:edit,post');
Multi-Tenant Architecture Patterns
Tenant Context Injection
<?php
namespace App\Http\Middleware;
use App\Models\Tenant;
use Closure;
use Illuminate\Http\Request;
class IdentifyTenant
{
public function handle(Request $request, Closure $next)
{
$tenant = $this->resolveTenant($request);
if (!$tenant) {
abort(404, 'Tenant not found');
}
// Inject tenant into application container
app()->instance('tenant', $tenant);
// Set tenant context for all queries
Tenant::setCurrent($tenant);
return $next($request);
}
protected function resolveTenant(Request $request): ?Tenant
{
// Method 1: Subdomain-based
if ($subdomain = $this->extractSubdomain($request)) {
return Tenant::where('subdomain', $subdomain)->first();
}
// Method 2: Header-based (for APIs)
if ($tenantId = $request->header('X-Tenant-ID')) {
return Tenant::find($tenantId);
}
// Method 3: Path-based
if ($request->segment(1)) {
return Tenant::where('slug', $request->segment(1))->first();
}
return null;
}
protected function extractSubdomain(Request $request): ?string
{
$host = $request->getHost();
$parts = explode('.', $host);
// Return subdomain if exists (not www or apex domain)
if (count($parts) > 2 && $parts[0] !== 'www') {
return $parts[0];
}
return null;
}
}
Tenant Database Connection Switching
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SwitchTenantDatabase
{
public function handle(Request $request, Closure $next)
{
$tenant = app('tenant');
// Configure tenant-specific database connection
config([
'database.connections.tenant' => [
'driver' => 'mysql',
'host' => config('database.connections.mysql.host'),
'database' => $tenant->database_name,
'username' => $tenant->database_user,
'password' => decrypt($tenant->database_password),
],
]);
// Purge existing connection and reconnect
DB::purge('tenant');
DB::reconnect('tenant');
// Set as default connection for this request
DB::setDefaultConnection('tenant');
return $next($request);
}
}
Learn about event-driven architecture for tenant-specific events.
Request Transformation Patterns
JSON API Response Formatting
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class FormatJsonResponse
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
if ($response instanceof JsonResponse) {
$data = $response->getData(true);
$formatted = [
'success' => $response->isSuccessful(),
'data' => $data,
'meta' => [
'timestamp' => now()->toIso8601String(),
'version' => config('app.api_version'),
],
];
if (!$response->isSuccessful()) {
$formatted['error'] = [
'code' => $response->status(),
'message' => $this->getErrorMessage($response->status()),
];
}
$response->setData($formatted);
}
return $response;
}
protected function getErrorMessage(int $status): string
{
return match($status) {
400 => 'Bad request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Resource not found',
422 => 'Validation failed',
429 => 'Too many requests',
500 => 'Internal server error',
503 => 'Service unavailable',
default => 'An error occurred',
};
}
}
Request Sanitisation
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class SanitizeInput
{
protected array $except = [
'password',
'password_confirmation',
'current_password',
];
public function handle(Request $request, Closure $next)
{
$input = $request->all();
foreach ($input as $key => $value) {
if (!in_array($key, $this->except) && is_string($value)) {
$request->merge([
$key => $this->sanitize($value),
]);
}
}
return $next($request);
}
protected function sanitize(string $value): string
{
// Remove NULL bytes
$value = str_replace(chr(0), '', $value);
// Strip HTML tags (except allowed ones)
$value = strip_tags($value, '<b><i><u><a><p><br>');
// Trim whitespace
$value = trim($value);
return $value;
}
}
API Versioning
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ApiVersioning
{
public function handle(Request $request, Closure $next)
{
$version = $this->resolveVersion($request);
// Inject version into container
app()->instance('api.version', $version);
// Set version in request attributes
$request->attributes->set('api_version', $version);
// Validate version is supported
if (!$this->isSupportedVersion($version)) {
return response()->json([
'error' => "API version {$version} is not supported",
'supported_versions' => config('api.supported_versions'),
], 400);
}
return $next($request);
}
protected function resolveVersion(Request $request): string
{
// Try header first
if ($version = $request->header('X-API-Version')) {
return $version;
}
// Try Accept header
$accept = $request->header('Accept');
if (preg_match('/application\/vnd\.api\+json;version=(\d+)/', $accept, $matches)) {
return 'v' . $matches[1];
}
// Try query parameter
if ($version = $request->query('version')) {
return $version;
}
// Default version
return config('api.default_version', 'v1');
}
protected function isSupportedVersion(string $version): bool
{
return in_array($version, config('api.supported_versions', ['v1']));
}
}
Review Laravel API design patterns for comprehensive versioning strategies.
Terminating Middleware
Understanding Terminating Middleware
Terminating middleware runs after the response has been sent to the browser. This is perfect for operations that don’t need to block the response.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class LogRequestDetails
{
public function handle(Request $request, Closure $next)
{
return $next($request);
}
/**
* Perform tasks after the response is sent to the browser.
*/
public function terminate(Request $request, $response): void
{
Log::info('Request completed', [
'url' => $request->fullUrl(),
'method' => $request->method(),
'user_id' => $request->user()?->id,
'ip' => $request->ip(),
'status' => $response->status(),
'duration' => microtime(true) - LARAVEL_START,
'memory' => memory_get_peak_usage(true),
]);
}
}
Performance Tracking
public function terminate(Request $request, $response): void
{
$duration = (microtime(true) - LARAVEL_START) * 1000; // milliseconds
// Track slow requests
if ($duration > 1000) {
Log::warning('Slow request detected', [
'url' => $request->fullUrl(),
'duration' => round($duration, 2) . 'ms',
'queries' => count(DB::getQueryLog()),
]);
}
// Send metrics to monitoring service
app('metrics')->timing('request.duration', $duration, [
'route' => $request->route()?->getName(),
'method' => $request->method(),
]);
}
Session Cleanup
public function terminate(Request $request, $response): void
{
// Clean up old session data
if ($user = $request->user()) {
$user->sessions()
->where('last_activity', '<', now()->subDays(30))
->delete();
}
// Clear temporary data
cache()->forget("temp.user.{$user?->id}");
}
Learn about Laravel performance optimisation for advanced tracking.
Middleware Groups and Stacks
Creating Custom Middleware Groups
// app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
// Standard web middleware
],
'api' => [
// Standard API middleware
],
'api.v1' => [
'throttle:60,1',
'auth:sanctum',
'api.version:v1',
],
'api.v2' => [
'throttle:100,1',
'auth:sanctum',
'api.version:v2',
'accept:json',
],
'admin' => [
'auth',
'role:admin',
'verified',
'activity.log',
],
'tenant' => [
'tenant.identify',
'tenant.database',
'tenant.features',
],
];
Usage:
// Apply API v1 middleware group
Route::prefix('v1')
->middleware('api.v1')
->group(function () {
Route::get('/users', [UserController::class, 'index']);
});
// Apply admin middleware group
Route::middleware(['web', 'admin'])
->prefix('admin')
->group(function () {
Route::get('/dashboard', [AdminController::class, 'dashboard']);
});
Priority and Order Control
// app/Http/Kernel.php
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\App\Http\Middleware\CheckRole::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
];
Middleware executes in this order when multiple are applied.
Advanced Patterns
Conditional Middleware Execution
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckFeatureFlag
{
public function handle(Request $request, Closure $next, string $feature)
{
$tenant = app('tenant');
if (!$tenant->hasFeature($feature)) {
if ($request->expectsJson()) {
return response()->json([
'error' => "Feature '{$feature}' not available",
], 403);
}
abort(403, "This feature is not available for your account.");
}
return $next($request);
}
}
Usage:
Route::get('/analytics', [AnalyticsController::class, 'index'])
->middleware('feature:analytics');
A/B Testing Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class ABTestVariant
{
public function handle(Request $request, Closure $next, string $experiment)
{
$variant = $this->getVariant($request, $experiment);
// Inject variant into application
app()->instance("experiment.{$experiment}", $variant);
// Add to request
$request->attributes->set('ab_variant', $variant);
// Track assignment
$this->trackAssignment($request, $experiment, $variant);
return $next($request);
}
protected function getVariant(Request $request, string $experiment): string
{
// Check if user already assigned
if ($user = $request->user()) {
$assignment = cache()->remember(
"experiment.{$experiment}.user.{$user->id}",
now()->addDays(30),
fn() => $this->assignVariant($user->id)
);
return $assignment;
}
// Anonymous users - use session
$sessionKey = "experiment.{$experiment}";
if (!session()->has($sessionKey)) {
session()->put($sessionKey, $this->assignVariant(session()->getId()));
}
return session($sessionKey);
}
protected function assignVariant(string $identifier): string
{
// 50/50 split
$hash = crc32($identifier);
return ($hash % 2 === 0) ? 'control' : 'variant';
}
protected function trackAssignment(Request $request, string $experiment, string $variant): void
{
// Track in analytics
event(new ExperimentAssigned($experiment, $variant, $request->user()));
}
}
Rate Limiting per User Role
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
class RoleBasedRateLimit
{
public function handle(Request $request, Closure $next)
{
$user = $request->user();
if (!$user) {
return $this->limitGuest($request, $next);
}
$limits = $this->getLimitsForUser($user);
$key = $this->resolveRequestKey($request, $user);
if (RateLimiter::tooManyAttempts($key, $limits['max_attempts'])) {
return $this->buildRateLimitResponse($key);
}
RateLimiter::hit($key, $limits['decay_seconds']);
$response = $next($request);
return $this->addRateLimitHeaders($response, $key, $limits);
}
protected function getLimitsForUser($user): array
{
return match(true) {
$user->hasRole('admin') => [
'max_attempts' => 1000,
'decay_seconds' => 60,
],
$user->hasRole('premium') => [
'max_attempts' => 200,
'decay_seconds' => 60,
],
default => [
'max_attempts' => 60,
'decay_seconds' => 60,
],
};
}
protected function resolveRequestKey(Request $request, $user): string
{
return 'rate_limit:' . $user->id . ':' . $request->ip();
}
protected function limitGuest(Request $request, Closure $next)
{
$key = 'rate_limit:guest:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 20)) {
return $this->buildRateLimitResponse($key);
}
RateLimiter::hit($key, 60);
return $next($request);
}
protected function buildRateLimitResponse(string $key)
{
$retryAfter = RateLimiter::availableIn($key);
return response()->json([
'error' => 'Too many requests',
'retry_after' => $retryAfter,
], 429)
->header('Retry-After', $retryAfter)
->header('X-RateLimit-Remaining', 0);
}
protected function addRateLimitHeaders($response, string $key, array $limits)
{
$remaining = RateLimiter::remaining($key, $limits['max_attempts']);
return $response
->header('X-RateLimit-Limit', $limits['max_attempts'])
->header('X-RateLimit-Remaining', $remaining);
}
}
Testing Middleware
Basic Middleware Testing
<?php
namespace Tests\Feature\Middleware;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CheckSubscriptionTest extends TestCase
{
use RefreshDatabase;
public function test_active_subscription_passes(): void
{
$user = User::factory()->withActiveSubscription()->create();
$response = $this->actingAs($user)
->get('/dashboard');
$response->assertOk();
}
public function test_expired_subscription_redirects(): void
{
$user = User::factory()->withExpiredSubscription()->create();
$response = $this->actingAs($user)
->get('/dashboard');
$response->assertRedirect(route('subscription.renew'));
$response->assertSessionHas('error');
}
public function test_no_subscription_redirects(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->get('/dashboard');
$response->assertRedirect(route('subscription.renew'));
}
}
Testing Middleware with Parameters
public function test_admin_role_grants_access(): void
{
$admin = User::factory()->withRole('admin')->create();
$response = $this->actingAs($admin)
->get('/admin/dashboard');
$response->assertOk();
}
public function test_non_admin_role_denies_access(): void
{
$user = User::factory()->withRole('user')->create();
$response = $this->actingAs($user)
->get('/admin/dashboard');
$response->assertForbidden();
}
Learn about Laravel testing strategies for comprehensive coverage.
Best Practices
Do:
Don’t:
Key Takeaways
- Understand the pipeline: Global → Group → Route middleware execution order
- Use parameters for flexible, reusable middleware
- Leverage middleware groups to avoid repetition and maintain consistency
- Implement terminating middleware for post-response operations
- Build multi-tenant architecture using context injection middleware
- Test thoroughly with feature tests covering all scenarios
- Monitor performance especially for global middleware
- Document clearly especially for complex or parameterised middleware
Complete Series Navigation
Continue mastering Laravel with the full series:
- Advanced Eloquent Relationships – Polymorphic relations and complex queries
- Custom Artisan Commands – CLI automation and task runners
- Advanced Middleware Patterns (this post) – Request processing and authentication
- Event-Driven Architecture – Events, listeners, and observers
- Advanced Testing Strategies – Comprehensive test coverage
- Performance Optimisation – Caching, query optimisation, profiling
- API Design and Documentation – RESTful APIs and OpenAPI
- Deployment and DevOps – CI/CD and production workflows
For AI-powered middleware generation:
- AI Code Generation – Automated middleware scaffolding
- Prompt Engineering Best Practices – Writing effective prompts
Ready to implement advanced middleware patterns? Contact us to discuss architecture strategies for your Laravel application.