Middleware
Every HTTP request that hits your Laravel application has to travel through your route to reach a controller, and the response has to travel back out again. Middleware is code that sits in that path and gets a chance to inspect, modify, or even reject the request before it ever reaches your route or controller — and it can do the same to the response on the way back out. It's a filtering and processing layer that wraps around the actual work your application does.
The Onion / Airport Security Analogy
Picture your application's core logic sitting at the center of an onion, with layers of middleware wrapped around it. A request has to pass through every layer to reach the center, and the response passes back out through those same layers in reverse. Or, think of airport security: before you reach your gate (the controller), you pass through checkpoints — ID check, bag scan, boarding pass verification. Each checkpoint can wave you through, or stop you right there and send you elsewhere. That's exactly what a piece of middleware does with a request: let it continue, or stop it and return a response immediately (a redirect to a login page, a 403 error, whatever's appropriate).
Middleware You've Probably Already Used
Laravel ships with several middleware classes out of the box, and if you've built anything with authentication you've almost certainly relied on these without writing a line of middleware yourself:
| Middleware | What it does |
|---|---|
auth | Checks whether a user is logged in. If not, it redirects the guest away from the protected route (typically to the login page) instead of letting the request reach the controller. |
guest | The exact opposite of auth — it only allows access if the user is not logged in. Useful on the login and registration pages themselves, so an already-logged-in user gets redirected to their dashboard instead of seeing the login form again. |
throttle | Rate limiting. Counts requests from the same source within a time window and returns a 429 "Too Many Requests" response once a limit is exceeded — commonly applied to login forms and APIs to prevent abuse. |
Creating a Custom Middleware
When the built-in middleware doesn't cover your case — say, restricting a section of the site to admin users only — you write your own. Artisan scaffolds the class for you:
# From your project's terminal
php artisan make:middleware EnsureUserIsAdmin
This generates app/Http/Middleware/EnsureUserIsAdmin.php with a single method already stubbed out for you: handle().
The handle($request, Closure $next) Method
Every middleware class implements one method, handle(), which receives the incoming
$request and a $next closure representing "the rest of the pipeline" —
every remaining middleware plus, eventually, the route's controller. Inside handle()
you have exactly two choices:
- Call
return $next($request);to let the request continue on to whatever comes next in the pipeline. - Return a response or redirect directly, without calling
$next()at all — which stops the request right there. The controller never runs.
Example: A Full EnsureUserIsAdmin Middleware
<?php
// app/Http/Middleware/EnsureUserIsAdmin.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next)
{
if (!auth()->check() || !auth()->user()->is_admin) {
return redirect('/')->with('error', 'You do not have access to that page.');
}
return $next($request);
}
}
If the logged-in user's is_admin column isn't truthy (or nobody is logged in at all),
the middleware redirects them home with an error message flashed to the session, and
$next($request) never even runs — the controller behind the route is never reached.
Any legitimate admin sails through, because the condition is false and execution falls through to
return $next($request);.
Registering Middleware
Writing the class isn't enough on its own — Laravel needs to know the middleware exists and what
short name to refer to it by. This is done by giving it an alias in the application's HTTP kernel
configuration (in modern Laravel, the middleware aliases live in bootstrap/app.php;
in older versions, in app/Http/Kernel.php's $routeMiddleware property).
Once aliased, you can attach it to any route using that short name.
<?php
// routes/web.php
Route::middleware('admin')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::get('/users', [UserController::class, 'index']);
});
This is the same Route::middleware()->group() pattern covered as a route group in
Routing: Advanced — middleware assignment is one of the
main reasons route groups exist in the first place, since it lets you protect a whole batch of
related routes with one line instead of repeating the middleware on every single route.
Global, Group, or Single-Route Middleware
Middleware can be applied at three different scopes, and picking the right one matters:
| Scope | When to use it |
|---|---|
| Global | Registered once and runs on every single request to the application, no matter the route. Reserved for things that truly apply everywhere — trimming whitespace from input, handling CORS, verifying CSRF tokens on web requests. |
| Route group | Applied with Route::middleware('name')->group(...) around several related routes at once — the most common choice for things like "all admin routes" or "all routes that require login." |
| Single route | Chained directly onto one specific route: Route::get(...)->middleware('admin'). Useful for a one-off route that needs different protection than its neighbors. |
Example: Single-Route Middleware
<?php
Route::get('/reports/export', [ReportController::class, 'export'])
->middleware('admin');
Route::middleware(['auth', 'admin'])->group(...) — Laravel runs them in the order
listed, and if any one of them stops the request, the rest (and the controller) never run.