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:

MiddlewareWhat it does
authChecks 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.
guestThe 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.
throttleRate 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.
Note: These aren't special or magical — they're the exact same kind of class you'll write yourself in the next section. Laravel just ships them pre-built because nearly every application needs them.

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:

ScopeWhen to use it
GlobalRegistered 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 groupApplied 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 routeChained 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');
You can also stack more than one middleware on the same route or group by passing an array: 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.
Next up: Now that requests can be filtered before reaching a controller, the next lesson covers what happens once they arrive there — reading input and validating it in Requests & Validation.