Middleware sits between the HTTP request and controller as a pipeline of checkpoints
Each middleware can inspect, modify, or short-circuit the request before it reaches the controller
Code before $next() runs on request inbound; code after runs on response outbound
Global middleware runs on every request; route middleware is opt-in per route or group
Middleware parameters let one class handle dynamic rules like role:editor,moderator
The pipeline uses Illuminate\Pipeline\Pipeline; failure to return $next($request) results in empty 200 response
✦ Definition~90s read
What is Laravel Middleware?
Laravel middleware is a series of HTTP request filters that execute before your controller logic runs. Think of it as a pipeline: every request passes through a stack of middleware classes, each one deciding whether to pass the request deeper or short-circuit with a response.
★
Picture a busy nightclub.
Under the hood, Laravel uses the Symfony HTTPKernel component's handle() method, wrapping each middleware in a closure that calls the next one — a classic decorator pattern. This architecture lets you layer authentication, logging, CORS, rate limiting, and authorization checks without cluttering your controllers.
The order matters: if you place a role-checking middleware before an auth middleware, unauthenticated users hit a 403 instead of a 401, because the role check fires first and rejects the request before Laravel ever verifies the user is logged in. This is a common pitfall that wastes hours of debugging — understanding the pipeline order is non-negotiable for production apps.
Plain-English First
Picture a busy nightclub. Before you reach the dance floor, you pass a bouncer who checks your ID, a coat-check who takes your jacket, and a staff member who stamps your hand. Each person does one job, in order, before you get inside. Laravel middleware is exactly that — a series of checkpoints every HTTP request must pass through before it ever touches your controller. If any checkpoint says 'no', the request never gets in.
Every serious web application has invisible rules running behind every single page load. Is this user logged in? Are they an admin? Should this response be cached? Is the session still valid? Without a clean system to answer those questions, you end up scattering security checks and response tweaks all over your controllers — a maintenance nightmare waiting to happen. Laravel middleware solves that by giving you a dedicated, organised layer that sits between the incoming request and your application logic.
The problem middleware solves is cross-cutting concerns — logic that applies to many routes but doesn't belong inside any single controller. Authentication is the classic example: you don't want to paste an if (!Auth::check()) { redirect('/login'); } block at the top of fifty different controller methods. Middleware lets you declare that concern once, attach it to whichever routes need it, and never think about it again. It keeps controllers lean and focused on their actual job: returning a response.
By the end of this article you'll know how to create custom middleware from scratch, understand the difference between global, route-level and group middleware, handle the 'after' vs 'before' execution distinction that trips up most developers, and build a real role-based access guard you could drop into a production app today. You'll also know exactly what to say when an interviewer asks about middleware pipelines.
What Laravel Middleware Actually Does Under the Hood
Laravel processes every request through a pipeline — a concept borrowed from Unix pipes. The pipeline takes your request object and passes it through a stack of middleware classes, one by one. Each middleware can inspect the request, modify it, short-circuit the whole pipeline by returning a response early, or pass the request down to the next middleware by calling $next($request).
This pipeline is powered by Illuminate\Pipeline\Pipeline and is assembled in your HTTP kernel (app/Http/Kernel.php). The kernel holds three lists: $middleware (global — runs on every request), $middlewareGroups (named collections like web and api), and $middlewareAliases (short names you attach to individual routes).
The key mental model is a Russian doll. Each middleware wraps the next one. When the innermost doll (your controller) produces a response, that response travels back outward through the same stack — meaning code written after$next($request) runs on the way out, not the way in. That's what makes 'before' vs 'after' middleware tick, and it's the thing almost everyone gets wrong the first time they build custom middleware.
RequestLifecycleDemo.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
// File: app/Http/Middleware/RequestLifecycleDemo.php// Purpose: Illustrate exactly WHEN code runs relative to the next layer.namespaceApp\Http\Middleware;
useClosure;
useIlluminate\Http\Request;
useIlluminate\Support\Facades\Log;
classRequestLifecycleDemo
{
publicfunctionhandle(Request $request, Closure $next): mixed
{
// ─────────────────────────────────────────────────────────// BEFORE phase: runs BEFORE the request reaches the controller.// Perfect for: authentication checks, rate limiting, input sanitising.// ─────────────────────────────────────────────────────────Log::info('⬇ Middleware BEFORE — request arriving', [
'url' => $request->fullUrl(),
'method' => $request->method(),
]);
// Hand the request to the next layer (another middleware or the controller).// $response contains whatever that next layer ultimately returned.
$response = $next($request);
// ─────────────────────────────────────────────────────────// AFTER phase: runs AFTER the controller has built the response.// Perfect for: adding headers, logging response times, caching.// ─────────────────────────────────────────────────────────
$response->headers->set('X-Processed-By', 'TheCodeForge');
Log::info('⬆ Middleware AFTER — response leaving', [
'status' => $response->getStatusCode(),
]);
return $response; // Send the (possibly modified) response back up the stack.
}
}
Output
// In storage/logs/laravel.log after one page visit:
[INFO] ⬇ Middleware BEFORE — request arriving {"url":"https://app.test/dashboard","method":"GET"}
[INFO] ⬆ Middleware AFTER — response leaving {"status":200}
// In the browser's Network tab → Response Headers:
X-Processed-By: TheCodeForge
Mental Model:
Think of $next($request) as a door hinge. Code above it runs on the way IN. Code below it runs on the way OUT. The controller sits in the middle and never knows the middleware exists.
Production Insight
A missing return $next($request) is the most common middleware bug. The middleware returns null, Laravel converts it to an empty 200 response — no error, no log. Only static analysis (PHPStan) catches it.
Pipeline order matters: global middleware runs first, then route middleware. If a global middleware modifies the request, route middleware sees the modified version.
Performance rule: keep global middleware lean — it runs on every request, including asset and health-check routes.
Key Takeaway
The pipeline is a nested Russian doll: each middleware wraps the next.
Code before $next() runs on request in; code after runs on response out.
Always explicitly return $next($request) or a response — null breaks silently.
thecodeforge.io
Laravel Middleware Order: Auth Before Role Prevents 403
Laravel Middleware
Creating Real-World Middleware — A Role-Based Access Guard
Let's build something you'd actually ship. Imagine a SaaS dashboard where certain routes are only accessible to users with an 'admin' role. We'll create an EnsureUserIsAdmin middleware that checks the authenticated user's role, redirects non-admins gracefully, and can be reused across any route with a single annotation.
Run php artisan make:middleware EnsureUserIsAdmin to generate the boilerplate, then fill in the logic. The generated file lands in app/Http/Middleware/. After writing the class, you register it in bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php (Laravel 10 and earlier) so the framework knows it exists.
Notice what the middleware does NOT do: it doesn't query the database for permissions lists, it doesn't render any HTML, and it doesn't touch the controller. Each of those concerns stays separate. The middleware has one job — decide whether this user is allowed through — and it does exactly that job, then gets out of the way.
This single-responsibility design is what makes middleware so composable. Need to add a 'super-admin' bypass later? Add a second middleware and stack it. Need to log every admin access? Add a logging middleware. None of them need to know about the others.
EnsureUserIsAdmin.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php
// File: app/Http/Middleware/EnsureUserIsAdmin.php// Run: php artisan make:middleware EnsureUserIsAdminnamespaceApp\Http\Middleware;
useClosure;
useIlluminate\Http\Request;
useIlluminate\Support\Facades\Auth;
useSymfony\Component\HttpFoundation\Response;
classEnsureUserIsAdmin
{
publicfunctionhandle(Request $request, Closure $next): Response
{
// If no one is logged in at all, send them to the login page.if (!Auth::check()) {
returnredirect()->route('login')
->with('status', 'Please log in to continue.');
}
// Auth::user() returns the currently authenticated User model.// We check a 'role' column on the users table — adjust to your schema.if (Auth::user()->role !== 'admin') {
// Abort with 403 Forbidden and a clear reason.// This gets caught by Laravel's exception handler,// which renders your custom 403.blade.php if it exists.abort(403, 'You do not have permission to access this area.');
}
// User is an admin — let the request continue normally.return $next($request);
}
}
// ─────────────────────────────────────────────────────────────────────────────// REGISTRATION — Laravel 11+ style (bootstrap/app.php)// ─────────────────────────────────────────────────────────────────────────────// In bootstrap/app.php://// use App\Http\Middleware\EnsureUserIsAdmin;//// ->withMiddleware(function (Middleware $middleware) {// $middleware->alias([// 'admin' => EnsureUserIsAdmin::class,// ]);// })// ─────────────────────────────────────────────────────────────────────────────// REGISTRATION — Laravel 10 style (app/Http/Kernel.php)// ─────────────────────────────────────────────────────────────────────────────// protected $middlewareAliases = [// 'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,// ];// ─────────────────────────────────────────────────────────────────────────────// USAGE IN ROUTES (routes/web.php)// ─────────────────────────────────────────────────────────────────────────────// routes/web.phpuseIlluminate\Support\Facades\Route;
useApp\Http\Controllers\AdminDashboardController;
useApp\Http\Controllers\ReportController;
// Single route — only admins can view the dashboard.Route::get('/admin/dashboard', [AdminDashboardController::class, 'index'])
->middleware('admin')
->name('admin.dashboard');
// Route group — apply 'admin' middleware to every route inside.Route::prefix('admin')
->middleware('admin')
->name('admin.')
->group(function () {
Route::get('/reports', [ReportController::class, 'index'])->name('reports');
Route::get('/users', [ReportController::class, 'users'])->name('users');
});
Use abort(403) instead of redirect()->back() for unauthorised admin access. A 403 is semantically correct (the resource exists, you just can't have it), it's loggable, and it prevents users from cycling through redirect loops if they somehow bookmark an admin URL.
Production Insight
Using redirect()->back() for unauthorized access causes redirect loops when the user bookmarks an admin URL. Use abort(403) instead — it's semantically correct, loggable, and doesn't confuse browsers.
Role checks often forget guest users. Always handle the unauthenticated case before checking role. Otherwise Auth::user() returns null and calling ->role throws an error.
The single-responsibility principle makes middleware testable: a middleware should only decide whether to allow or deny, not fetch permissions or render HTML.
Key Takeaway
One middleware class with a single job: allow or deny.
Use abort(403) for unauthorized access, not redirects.
Test with guest, non-admin, and admin to cover all authorization paths.
Middleware Parameters and Chaining — Advanced Patterns You'll Actually Use
Hard-coding role checks inside middleware is fine for simple cases, but what if you have four roles — 'admin', 'editor', 'moderator', 'viewer'? You'd need four separate middleware classes. Middleware parameters solve this elegantly: you pass a dynamic value through the route definition, and your middleware receives it as an extra argument after $next.
Chaining is the other power move. You can stack multiple middleware on a single route, and they execute in left-to-right order. This lets you compose complex access rules from simple, reusable pieces — auth to check login, then verified to check email confirmation, then role:editor to check the role. Each piece stays independently testable.
Parameters and chaining together give you a tiny permissions DSL built right into your route file — readable, auditable, and requiring zero changes to controller code when your access rules change.
EnsureUserHasRole.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
// File: app/Http/Middleware/EnsureUserHasRole.php// A single middleware that handles ANY role via a parameter.namespaceApp\Http\Middleware;
useClosure;
useIlluminate\Http\Request;
useIlluminate\Support\Facades\Auth;
useSymfony\Component\HttpFoundation\Response;
classEnsureUserHasRole
{
/**
* @param string $requiredRole — injected by Laravel from the route definition.
* e.g. middleware('role:editor') sets $requiredRole = 'editor'
* @param string ...$extraRoles — variadic: middleware('role:editor,moderator') passes both.
*/
publicfunctionhandle(
Request $request,
Closure $next,
string $requiredRole,
string ...$extraRoles // Capture any additional roles passed after a comma.
): Response {
// Merge the first role and any extras into one array for a clean in_array check.
$allowedRoles = [$requiredRole, ...$extraRoles];
$currentUserRole = Auth::user()?->role; // ?-> safely returns null if not authenticated.if (!in_array($currentUserRole, $allowedRoles, strict: true)) {
abort(403, "Access requires one of: " . implode(', ', $allowedRoles));
}
return $next($request);
}
}
// ─────────────────────────────────────────────────────────────────────────────// REGISTRATION (bootstrap/app.php — Laravel 11+)// ─────────────────────────────────────────────────────────────────────────────// $middleware->alias([// 'role' => \App\Http\Middleware\EnsureUserHasRole::class,// ]);// ─────────────────────────────────────────────────────────────────────────────// USAGE — Chaining middleware on routes (routes/web.php)// ─────────────────────────────────────────────────────────────────────────────useIlluminate\Support\Facades\Route;
useApp\Http\Controllers\ArticleController;
useApp\Http\Controllers\AnalyticsController;
// Chain: user must be logged in AND have a verified email AND have the 'editor' role.Route::get('/articles/create', [ArticleController::class, 'create'])
->middleware(['auth', 'verified', 'role:editor'])
->name('articles.create');
// Multiple roles allowed — editor OR moderator can access analytics.Route::get('/analytics', [AnalyticsController::class, 'index'])
->middleware(['auth', 'role:editor,moderator'])
->name('analytics.index');
// Route group with shared middleware — every route here needs auth + admin role.Route::middleware(['auth', 'role:admin'])
->prefix('admin')
->name('admin.')
->group(function () {
Route::get('/settings', fn() => view('admin.settings'))->name('settings');
Route::get('/billing', fn() => view('admin.billing'))->name('billing');
});
Output
// GET /articles/create as a logged-in, email-verified 'editor':
// → HTTP 403: "Access requires one of: editor, moderator"
Watch Out:
When chaining middleware like ['auth', 'role:editor'], order matters. If you put 'role:editor' before 'auth', the role middleware will call Auth::user() on a guest and get null — then crash or silently fail. Always authenticate before authorising.
Production Insight
Variadic parameters let one middleware handle multiple roles with a single class, reducing boilerplate. However, role strings in route definitions become a maintenance burden if roles change frequently. Consider a permission system with a database lookup instead.
Chaining order is critical: authentication must come before authorization. A common incident: putting 'role:editor' before 'auth' causes a 500 error on guest requests because Auth::user() is null.
Group middleware with prefix and name keeps route files clean but can hide middleware from new developers. Document the group middleware in the route file comment.
Key Takeaway
Middleware parameters create a DSL for access control in route files.
Testing Middleware in Isolation — Don't Skip This Step
Most developers test middleware indirectly by hitting a route and checking the response code. That works, but it makes your tests fragile — a controller change can break a middleware test for no good reason. Laravel's testing toolkit lets you test middleware directly by faking the request and injecting it into the middleware's handle method. This keeps your middleware tests fast, focused, and immune to controller-level changes.
The cleaner approach for route-level tests is $this->actingAs($user) combined with ->get('/route') and asserting the HTTP status code. For middleware unit tests, you can also use Route::fake() or test the middleware class directly with a Request and a closure that captures whether $next was called.
Pair middleware tests with Laravel's RefreshDatabase trait when the middleware queries the database (like a subscription check), and avoid it for pure logic checks (like a role string comparison) to keep your suite fast. Good middleware tests document your security rules better than any comment ever could.
EnsureUserHasRoleTest.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php
// File: tests/Feature/Middleware/EnsureUserHasRoleTest.php// Run: php artisan test --filter=EnsureUserHasRoleTestnamespaceTests\Feature\Middleware;
useApp\Models\User;
useIlluminate\Foundation\Testing\RefreshDatabase;
useTests\TestCase;
classEnsureUserHasRoleTestextendsTestCase
{
use RefreshDatabase; // Resets the DB between tests — important for user creation.
/** @test */
publicfunctionadmin_user_can_access_admin_dashboard(): void
{
// Arrange: create a user with the 'admin' role using a factory state.
$adminUser = User::factory()->create(['role' => 'admin']);
// Act: simulate the admin making a GET request, fully authenticated.
$response = $this->actingAs($adminUser)->get('/admin/dashboard');
// Assert: the request passed through middleware and hit the controller.
$response->assertStatus(200);
}
/** @test */
publicfunctioneditor_user_is_forbidden_from_admin_dashboard(): void
{
// Arrange: a regular editor — NOT an admin.
$editorUser = User::factory()->create(['role' => 'editor']);
// Act + Assert: expect the middleware to block with 403.
$this->actingAs($editorUser)
->get('/admin/dashboard')
->assertStatus(403);
}
/** @test */
publicfunctionguest_is_redirected_to_login_from_admin_dashboard(): void
{
// No actingAs() — simulates an unauthenticated guest.
$this->get('/admin/dashboard')
->assertRedirect('/login');
}
/** @test */
publicfunctionmiddleware_allows_multiple_roles_when_configured(): void
{
// Arrange: a moderator hitting the analytics route which allows editor,moderator.
$moderator = User::factory()->create(['role' => 'moderator']);
$this->actingAs($moderator)
->get('/analytics')
->assertStatus(200); // Moderator is in the allowed list — should pass.
}
}
Output
// php artisan test --filter=EnsureUserHasRoleTest
✓ editor user is forbidden from admin dashboard 0.09s
✓ guest is redirected to login from admin dashboard 0.08s
✓ middleware allows multiple roles when configured 0.10s
Tests: 4 passed (4 assertions)
Duration: 0.45s
Interview Gold:
Interviewers love asking 'how do you test middleware?'. The answer that impresses is: 'I test the HTTP contract with actingAs() at the feature level, which verifies the middleware is correctly registered and enforced on the right routes — not just that the class logic works.' It shows you understand testing at the right layer of abstraction.
Production Insight
Feature tests with actingAs() and status assertions are the most reliable way to test middleware because they verify both the middleware logic and its registration on the correct routes. Unit testing the handle method alone misses registration bugs.
Using RefreshDatabase for tests that create users ensures clean state, but avoid it for middleware tests that don't touch the database (e.g., check a header). Keep tests fast by using plain HTTP requests without DB setup when possible.
A common oversight: testing only the happy path (200) and forgetting the 403 and redirect cases. Cover all three outcomes for robust test coverage.
Key Takeaway
Test middleware at the HTTP contract level — not the method level.
Cover three outcomes: 200 (allowed), 403 (denied), redirect (unauthenticated).
Use actingAs() to simulate authentication in feature tests.
Excluding Middleware and Route Group Tricks — withoutMiddleware() and Middleware Priority
Sometimes you need a route inside a group to skip a middleware that the group applies. For example, an admin group might have 'auth' and 'role:admin', but a public login route inside that group shouldn't require authentication. Laravel's ->withoutMiddleware() method lets you exclude specific middleware from a route or group.
withoutMiddleware() is also useful when you're testing a route in isolation and don't want any global middleware interfering. Combined with route groups, it gives you fine-grained control over which requests pass through which gates.
Another advanced trick: middleware priority. Laravel allows you to define a $middlewarePriority array in your HTTP kernel to force the order of middleware (e.g., always run StartSession before Authenticate). This is rarely needed but valuable when you have middleware that depends on another's side effects (like sessions).
withoutMiddleware_example.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
// routes/web.phpuseIlluminate\Support\Facades\Route;
useApp\Http\Controllers\Auth\LoginController;
useApp\Http\Controllers\DashboardController;
// Admin group with auth and role:admin middlewareRoute::middleware(['auth', 'role:admin'])
->prefix('admin')
->group(function () {
// This route will NOT have the 'auth' and 'role:admin' middleware appliedRoute::get('/public-info', function () {
return'Public admin info - no auth required';
})->withoutMiddleware(['auth', 'role:admin']);
// Other routes in the group still use the group middlewareRoute::get('/dashboard', [DashboardController::class, 'index']);
});
// ─────────────────────────────────────────────────────────────────────────────// withoutMiddleware() also works for individual middleware:// Route::get('/some-route', ...)->withoutMiddleware('auth');//// ─────────────────────────────────────────────────────────────────────────────// Middleware Priority in app/Http/Kernel.php (Laravel 10+)://// protected $middlewarePriority = [// \Illuminate\Session\Middleware\StartSession::class,// \Illuminate\View\Middleware\ShareErrorsFromSession::class,// \App\Http\Middleware\Authenticate::class,// \Illuminate\Routing\Middleware\SubstituteBindings::class,// // ...// ];//// This forces StartSession to always run before Authenticate, even if the order in
the route middleware array is different.
Output
// GET /admin/public-info → returns "Public admin info - no auth required"
// (no authentication required, passes through without the group middleware)
// GET /admin/dashboard → requires authentication and admin role
// (group middleware applies normally)
Caveat:
withoutMiddleware() only works for route/group middleware, not global middleware. If you need to skip a global middleware, you'll need to modify the $middleware array or use a condition inside the middleware itself. Also, withoutMiddleware() was introduced in Laravel 6.x; older versions may require workarounds.
Production Insight
Overusing withoutMiddleware() can lead to security gaps — always verify that exclusion is intentional and documented. A common incident: a developer excluded 'auth' from a route but forgot that it also exposed an admin endpoint to unauthenticated users.
Middleware priority ordering should be set once and not changed lightly. It affects every request. Prefer explicit chaining in route definitions over priority for clarity.
Using withoutMiddleware() in tests is helpful to isolate a single middleware, but don't rely on it for production routes — it can hide registration issues.
Key Takeaway
Use ->withoutMiddleware() sparingly and always document the reason.
Middleware priority forces order globally; use only when necessary.
Test excluded routes explicitly to verify they are intentionally unprotected.
Middleware Execution Order: Why Your Filters Run in the Wrong Sequence
Most tutorials show you how to create middleware but never explain order. This is where bugs breed. Laravel runs middleware in a stack. Global middleware fires first, then route-specific middleware (in the order you assign them), and finally controller middleware. But here’s the kicker: the response runs in reverse. If your logging middleware runs before auth, you’ll log requests that fail authentication. Learn to control priority. In app/Http/Kernel.php, the $middlewarePriority array lets you reorder global middleware. I’ve seen teams spend hours debugging CORS issues caused by middleware order. Always put EncryptCookies before StartSession. Put Cors before ThrottleRequests if you need to return CORS headers on throttled responses. Test order explicitly in your route files. Don’t assume the stack is default-safe.
app/Http/Kernel.phpPHP
1
2
3
4
5
6
7
8
// Priority sorted list of middlewareprotected $middlewarePriority = [
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\LogRequest::class, // MUST come after session
\Illuminate\Auth\Middleware\Authenticate::class,
];
Output
// With this order: session is active before logging, so logs can include user_id.
// Reverse on response ensures CORS headers are appended last.
Production Trap:
If your API gateway returns vague 500 errors, check middleware order first. I once saw ThrottleRequests run before HandleCors — browsers blocked the 429 response because CORS headers were missing. Reorder saved the feature.
Key Takeaway
Middleware order is a hidden dependency graph. Always test the request-response round trip.
Terminable Middleware: Run Cleanup After the Response Ships
Most middleware runs before or after the controller. But what about code that should execute after the response is already sent to the browser? Laravel’s terminable middleware runs during kernel termination. Use it for writing audit logs, flushing cache, or sending analytics without blocking the user. Define a terminate method on any middleware. Laravel calls it after the response is sent. This is not async — it’s synchronous but happens after headers are flushed. Perfect for heavy operations like image resizing or slow API calls. Benchmark your routes. If a middleware takes 200ms and you move it to terminate, the user sees the page in real-time while the server finishes work. Think of it as a deferred payload. I use this in payment systems to log receipts without slowing checkout.
Use terminable middleware sparingly. In high-traffic apps, the deferred load still consumes PHP-FPM threads. For truly async tasks, push jobs to a queue instead. Terminate is best for logging and metrics collection.
Key Takeaway
Terminable middleware lets you send the response now and clean up later — the closest thing to async in synchronous PHP.
Trusted Proxy Middleware: How to Stop Breaking Redirects Behind Load Balancers
Every Laravel app behind a proxy (Nginx, AWS ELB, Cloudflare) needs TrustProxies middleware. Without it, redirect()->back() sends users to http://localhost instead of your domain. The url() helper generates wrong scheme (http vs https). I’ve seen teams blame Laravel for broken login redirects when the fix was a single line. Open App\Http\Middleware\TrustProxies. Set $proxies to '*' if you trust all proxies (like Cloudflare), or list specific IPs. Configure $headers to Request::HEADER_X_FORWARDED_TRAFO for AWS. Test with dd(url('home')) before deploying. A misconfigured proxy middleware creates silent security holes — cookie theft via mixed content warnings. Trust me, this is the most overlooked middleware in production.
// Before: https://app/secure-page redirects to http://app/secure-page
// After: all generated URLs use https, redirect()->back() works correctly
Production Trap:
Never set $proxies = '*' on shared hosting you don’t fully control. An attacker could spoof headers and bypass IP-based rate limiting. On AWS, set $proxies = [env('AWS_ELB_IP')] or use \Symfony\Component\HttpFoundation\Request::setTrustedProxies with a whitelist.
Key Takeaway
Always configure TrustProxies before deploying any Laravel app behind a proxy — one line fixes all redirect and scheme bugs.
● Production incidentPOST-MORTEMseverity: high
Middleware Order Bug Takes Down Admin Panel in Production
Symptom
All users, including admins, received HTTP 403 when accessing /admin/dashboard. No error in logs. Authentication worked fine on other routes.
Assumption
Admin middleware was correctly checking Auth::user()->role.
Root cause
The middleware chain was ['role:admin', 'auth']. The role middleware executed before the auth middleware, so Auth::user() returned null, causing abort(403) for everyone.
Fix
Reorder the middleware chain to ['auth', 'role:admin'] in the route group definition.
Key lesson
Always place authentication middleware before authorization middleware in the chain.
Test middleware order with a guest user first: if guest gets 403 instead of redirect, order is wrong.
Use a logging middleware to trace the pipeline execution order during debugging.
Production debug guideQuick diagnosis of common middleware failures4 entries
Symptom · 01
Blank page with 200 status on a route
→
Fix
Check if middleware returns $next($request). Missing return causes null response.
Symptom · 02
Class 'xyz' not found when using middleware alias
→
Fix
Verify middleware alias is registered in bootstrap/app.php (Laravel 11) or Kernel.php (Laravel 10). Alias must match route definition exactly.
Symptom · 03
403 on route even for authenticated users
→
Fix
Inspect middleware chain order. Ensure 'auth' comes before any role/authorization middleware.
Symptom · 04
Middleware redirect loop on admin routes
→
Fix
Use abort(403) instead of redirect()->back() for unauthorized access to prevent infinite loop.
Runs after global middleware, in declaration order
Key takeaways
1
Code written BEFORE $next($request) runs on the request going IN
use it for auth, rate limiting, and input checks. Code written AFTER runs on the response coming OUT — use it for headers, logging, and caching.
2
Middleware parameters (middleware('role:editor,moderator')) let one class handle dynamic rules, eliminating the need for a separate middleware class per role or permission level.
3
Middleware chain order is security-critical
always run 'auth' before any authorisation middleware that calls Auth::user(), or you'll get null pointer errors or silent security failures.
4
Test middleware at the HTTP contract level with actingAs()
asserting the correct status codes (200, 403, redirect) proves both that the middleware logic is correct AND that it's registered on the right routes.
5
Middleware can be excluded from specific routes using withoutMiddleware(), but use it sparingly and document the exclusion to avoid accidental security gaps.
Common mistakes to avoid
3 patterns
×
Forgetting to return $next($request)
Symptom
The middleware silently returns null, which Laravel converts to an empty HTTP response with no content and no error message. Every request to that route returns a blank page with a 200 status.
Fix
Always explicitly return the result of $next($request). Use PHPStan or Larastan to catch missing return statements at static analysis time.
×
Registering middleware but not aliasing it
Symptom
You add your class to Kernel.php's $routeMiddleware (Laravel 10) or forget the alias() call in bootstrap/app.php (Laravel 11), then use ->middleware('mymiddleware') on a route. Laravel throws 'Class mymiddleware does not exist'.
Fix
The class name and the alias are two different things. The alias ('role', 'admin', 'throttle') is the short string you use in route files. The class is the full PHP class. Both must be registered. For Laravel 11: ensure ->withMiddleware() includes the $middleware->alias() call.
×
Placing authorisation logic before authentication in a middleware chain
Symptom
Writing ->middleware(['role:admin', 'auth']) instead of ['auth', 'role:admin']. The role middleware calls Auth::user() before the auth middleware has validated the session, getting null back. This either throws a 'Call to a member function role() on null' error or silently fails the role check and returns 403 to everyone, including logged-in admins.
Fix
Authentication always comes first in the chain. Think of it as: 'prove who you are before we check what you're allowed to do'.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Can you explain what the Laravel middleware pipeline is and how a reques...
Q02SENIOR
What is the difference between before middleware and after middleware in...
Q03SENIOR
If you register a middleware alias and apply it to a route group, but on...
Q04SENIOR
How do you pass parameters to middleware in Laravel? Can you pass multip...
Q05SENIOR
What is the $middlewarePriority array and when would you use it?
Q01 of 05SENIOR
Can you explain what the Laravel middleware pipeline is and how a request moves through it? What happens to the response on the way back out?
ANSWER
The middleware pipeline is a stack of middleware classes through which every HTTP request passes. Each middleware receives the request and a $next closure. Code before $next($request) runs on the way in; the middleware then calls $next($request) to pass control to the next middleware (or the controller). After the controller returns a response, the response flows back through the same stack, and code after $next($request) runs on the way out. This allows middleware to both inspect/modify the request before the controller and modify the response after. The pipeline is implemented by Illuminate\Pipeline\Pipeline and assembled in the HTTP kernel.
Q02 of 05SENIOR
What is the difference between before middleware and after middleware in Laravel? Give a real use case for each.
ANSWER
Before middleware runs code entirely before $next($request) is called. It's used for authentication, rate limiting, input sanitization — actions that must happen before the request reaches the controller. After middleware runs code after $next($request) returns a response. It's used for adding headers (Content-Security-Policy, CORS), logging response times, compressing output, or caching the response. In code: before middleware has no code after $next(), after middleware has code after $next() that modifies or logs the $response.
Q03 of 05SENIOR
If you register a middleware alias and apply it to a route group, but one route inside that group needs to skip the middleware, how would you handle that?
ANSWER
You can use the withoutMiddleware() method on the specific route inside the group. For example: Route::get('/public-info', [SomeController::class, 'index'])->withoutMiddleware('auth'); This removes the 'auth' middleware from that route while all other routes in the group still have it. Note that withoutMiddleware() only works for route/group middleware, not global middleware. If you need to conditionally apply middleware, you can also use a closure in the route definition or check inside the middleware itself.
Q04 of 05SENIOR
How do you pass parameters to middleware in Laravel? Can you pass multiple parameters?
ANSWER
Middleware parameters are passed via the route definition using a colon syntax: 'role:editor' passes 'editor' as the first parameter after $next. For multiple parameters, you can separate with commas: 'role:editor,moderator' passes 'editor' as the first parameter and 'moderator' as the second. In the middleware handle method, you accept these as additional string arguments. You can also use variadic arguments via ...$extraRoles to capture an arbitrary number of parameters. Middleware parameters must be registered with an alias in the HTTP kernel.
Q05 of 05SENIOR
What is the $middlewarePriority array and when would you use it?
ANSWER
The $middlewarePriority array in app/Http/Kernel.php allows you to define the order in which middleware runs, overriding the declaration order. It's useful when a middleware depends on the side effects of another middleware (e.g., you need the session to be started before the authentication middleware runs). You set an ordered array of middleware class names, and Laravel ensures they run in that order regardless of their position in the route middleware array. Use it sparingly — it's global and affects every request. Prefer explicit chaining for most cases.
01
Can you explain what the Laravel middleware pipeline is and how a request moves through it? What happens to the response on the way back out?
SENIOR
02
What is the difference between before middleware and after middleware in Laravel? Give a real use case for each.
SENIOR
03
If you register a middleware alias and apply it to a route group, but one route inside that group needs to skip the middleware, how would you handle that?
SENIOR
04
How do you pass parameters to middleware in Laravel? Can you pass multiple parameters?
SENIOR
05
What is the $middlewarePriority array and when would you use it?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
How do I create custom middleware in Laravel?
Run php artisan make:middleware YourMiddlewareName. This creates a class in app/Http/Middleware/ with a handle(Request $request, Closure $next) method. Write your logic in that method, then register the class in bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php (Laravel 10) and assign it an alias to use in your route definitions.
Was this helpful?
02
What is the difference between global middleware and route middleware in Laravel?
Global middleware runs on every single HTTP request your application receives, regardless of which route is hit — things like trimming whitespace from inputs or setting CORS headers. Route middleware is opt-in: you explicitly attach it to specific routes or route groups, so it only runs when those routes are matched. Put security-critical, route-specific logic in route middleware to avoid unnecessary overhead on every request.
Was this helpful?
03
Can Laravel middleware modify the response, or does it only inspect the request?
It can do both. By placing code before $next($request) you inspect or modify the incoming request. By placing code after $next($request) — on the returned $response object — you can add headers, log response data, compress output, or even replace the response entirely. This 'before and after' dual capability is what makes middleware so powerful for cross-cutting concerns.
Was this helpful?
04
Can I apply middleware to all routes except a few?
Yes, you can use withoutMiddleware() on specific routes to exclude them from group or route middleware. However, this does not work for global middleware. You can also conditionally apply middleware inside the handle method by checking the route name or URL pattern, but that couples the middleware to route details. The recommended approach for many exceptions is to apply middleware to individual routes rather than to a group.
Was this helpful?
05
How do I test that middleware is correctly registered and enforced?
The best way is to write feature tests that simulate requests to routes with the middleware applied. Use actingAs() to authenticate users with different roles, then assert the expected HTTP status codes (200, 403, redirect). Also test guest access. This verifies that the middleware is both logically correct and registered on the intended routes. You can also unit test the handle method directly, but feature tests catch registration issues.