Laravel middleware is a filtering layer that runs before an HTTP request reaches the inner parts of your application and before the response goes back to the browser. Think of it as a series of gates sitting between the request and your controller: each gate inspects the request and can stop it, modify it, or pass it on to the next gate. Almost everything like authentication, session handling, CSRF protection and locale detection is done with middleware.
The role of middleware in the request/response chain
In Laravel every incoming request travels through a "pipeline". This pipeline works like an onion: the request passes through each layer from the outside in until it reaches the controller, and the response then travels back out through the same layers in reverse. Because of this, a single middleware can run code both before the request and after the response.
- Code that runs before: checks the request before it reaches the controller (for example, is the user authenticated?).
- Code that runs after: does work once the response is produced, like adding a header or writing a log entry.
Order in the chain matters. The middleware that starts the session, for instance, has to run before any session-dependent verification. Laravel sets a sensible default order, but in your own middleware you need to mind where it sits in the stack.
How middleware works: the basic structure
Every middleware is a simple class with a handle method. That method takes two parameters: the incoming $request and a closure called $next. Calling $next($request) passes the request on to the next link in the chain.
public function handle(Request $request, Closure $next)
{
// Pre-request logic goes here
$response = $next($request);
// Post-response logic goes here
return $response;
}
Everything you write before the $next($request) line runs before the request reaches the controller; everything after it runs as the response travels back. If you don't want the request to continue at all (for example when access is denied) you simply return a response without calling $next.
Writing your own middleware
The most practical way to create a new middleware is the Artisan command:
php artisan make:middleware EnsureTokenIsValid
This generates the file app/Http/Middleware/EnsureTokenIsValid.php. Say we want to reject any request that doesn't carry a valid token in a given request header:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureTokenIsValid
{
public function handle(Request $request, Closure $next): Response
{
if ($request->header('X-Api-Token') !== config('services.api.token')) {
abort(403, 'Invalid token.');
}
return $next($request);
}
}
Here, if the token doesn't match, abort(403) cuts the chain immediately and the request never reaches any controller. If it matches, the flow continues with $next($request).
Registering middleware (Laravel 11 and 12)
As of Laravel 11, middleware registration moved from the old app/Http/Kernel.php file into bootstrap/app.php. There are three main ways to register it:
- Global middleware: applied to every request.
- Group middleware: added to route groups such as
weborapi. - Aliased middleware: assigned to individual routes with
->middleware('alias').
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
// Define an alias to use on individual routes
$middleware->alias([
'token' => \App\Http\Middleware\EnsureTokenIsValid::class,
]);
// Append to the web group
$middleware->web(append: [
\App\Http\Middleware\SetLocale::class,
]);
})
->create();
Once the alias is defined you use it on routes like this:
Route::get('/dashboard', DashboardController::class)
->middleware('token');
Note: if you're still on Laravel 10 or earlier, the same registrations are done in the $middlewareAliases and $middlewareGroups arrays inside app/Http/Kernel.php.
Middleware that takes parameters
Middleware can also accept extra arguments. Laravel's built-in authorization and role checks work exactly on this principle. You add parameters to the handle method after $next:
public function handle(Request $request, Closure $next, string $role): Response
{
if (! $request->user()?->hasRole($role)) {
abort(403);
}
return $next($request);
}
On the route you pass the value after a colon:
Route::get('/admin', AdminController::class)
->middleware('role:editor');
Common mistakes
- Forgetting to return
$next($request): if the method doesn't return a response you get a blank page or an error. Unless you're exiting early, always writereturn $next($request). - Wrong order: if session-dependent work runs before the session middleware, you get unexpected results.
- Heavy work: middleware runs on every request; use database queries and external API calls sparingly here, and cache them where possible.
Frequently Asked Questions
What's the difference between middleware and a controller?
A controller runs the actual business logic of a specific route. Middleware is a cross-cutting layer that applies general filtering or checks to the request and repeats across many routes. Work like authentication belongs in middleware, not the controller.
Can I add more than one middleware to a route?
Yes. You pass an array like ->middleware(['auth', 'token', 'role:editor']); they're added to the chain in the order you write them.
Can I exclude global middleware from certain routes?
Usually the cleaner solution is to add the middleware to the relevant group or routes via an alias instead of making it global. That way you control exactly where it runs.
Want to set up the middleware architecture in your Laravel project the right way? From authentication to multi-language detection, I can help you design a clean, testable middleware layer. Get in touch with me.