Middleware in Laravel 13: Create, Register, and Assign Custom Middleware
Every HTTP request to your app passes through a middleware stack before reaching the route: auth, locale, and security are all middleware, and in Laravel 13 every one of them is registered in bootstrap/app.php with withMiddleware(). This guide shows you how to build your own with real code.
What Middleware Is and Where It Fits in Laravel 13
A Layer Between the HTTP Request and the Route
Middleware is a convenient mechanism for inspecting and filtering the HTTP requests entering your application before they reach the controller. The canonical example from the official docs is checking that a user is authenticated before letting them into a protected route: if not, redirect to the login page; if yes, let the request continue. But the same pattern covers far more ground: limiting requests per minute (throttle), adding security headers, detecting the visitor's language, or flipping on maintenance mode.
Before and After the Response: the handle() Method and $next
Every middleware defines a handle(Request $request, Closure $next) method. Anything you write before return $next($request) runs as the request comes in, and anything after it runs once the controller has produced the response. That lets you both block a request outright (returning a response without ever calling $next) and modify the outgoing response, for example by adding a header. That dual ability is what makes middleware so powerful: a single class can act as an input filter and an output decorator at the same time.
Creating Middleware with Artisan
php artisan make:middleware and the Class Structure
Laravel scaffolds the whole thing for you with a single command:
Read also
php artisan make:middleware EnsureLocaleIsValidIt creates the class in app/Http/Middleware/EnsureLocaleIsValid.php with the handle() skeleton ready to fill in:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureLocaleIsValid
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
}By convention, code that runs before the request goes before return $next($request), and code that runs after goes after it, manipulating the returned response. Nothing more to it.
Inline Closure Middleware Directly in Your Routes
For small, one-off logic you do not need a class at all. You can pass a Closure as middleware right in the route definition:
Route::get('/dashboard', function () {
// ...
})->middleware(function (Request $request, Closure $next) {
if ($request->user()?->isAdmin() !== true) {
abort(403);
}
return $next($request);
});It is handy for prototypes, but if the logic shows up in several routes, promote it to a class.
Registering and Assigning Middleware in Laravel 13
bootstrap/app.php and withMiddleware(): Goodbye to Kernel.php
Since Laravel 11 there is no app/Http/Kernel.php anymore, and Laravel 13 keeps that approach. All middleware configuration lives in bootstrap/app.php inside the withMiddleware() method:
->withMiddleware(function (Middleware $middleware) {
// everything is configured here
})That is where you define global middleware, extend the web and api groups, and register aliases for your classes.
Global Middleware: Appending and Prepending
Global middleware runs on every request the application receives, no exceptions. You add it with append() (at the end of the stack) or prepend() (at the start):
->withMiddleware(function (Middleware $middleware) {
$middleware->append(AddSecurityHeaders::class);
})Use it sparingly: every global middleware adds work to each request, and a huge stack becomes hard to debug.
The Web and API Groups: appendToGroup
Laravel ships two default groups: web (routes with sessions and cookies) and api (routes with throttle and CORS). You can add your own middleware to either with appendToGroup():
->withMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup('web', EnsureLocaleIsValid::class);
})The middleware then applies to every route in that group without mentioning it on each one.
Assigning to Routes and Route Groups: ->middleware()
To apply middleware only to specific routes, assign it in the route definition itself, using the class name or an alias:
Route::get('/profile', [ProfileController::class, 'edit'])
->middleware('auth');
Route::prefix('/admin')->middleware(['auth', 'role:admin'])->group(function () {
Route::resource('posts', PostController::class);
});The method accepts a string or an array and works on single routes as well as route groups, letting you stack several middleware in one line.
Aliases: Short Names for Your Classes
Laravel provides default aliases for its built-in middleware: auth, guest, signed, throttle, verified, and more. Your own aliases are registered in withMiddleware() with alias():
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'locale' => EnsureLocaleIsValid::class,
'role' => EnsureUserHasRole::class,
]);
})From then on you can use ->middleware('locale') on any route with a short, readable name.
Middleware with Parameters
Passing Arguments After $next: middleware('role:admin')
Middleware can receive extra parameters with the alias:parameter syntax. The value arrives in handle() as an extra argument after $next:
public function handle(Request $request, Closure $next, string $role): Response
{
if (! $request->user() || ! $request->user()->hasRole($role)) {
abort(403);
}
return $next($request);
}Route::get('/admin/posts', ...)->middleware('role:admin');This is the typical pattern for roles and permissions, and it lets you reuse the same class with different values on different routes, like role:admin and role:editor.
Terminable Middleware: Code After the Response Is Sent
The terminate() Method and the FastCGI Requirement
If your middleware defines a terminate($request, $response) method, that code runs after the response has been sent to the browser, as long as the server uses FastCGI (like PHP-FPM). It is perfect for tasks that should not delay the response:
public function terminate(Request $request, Response $response): void
{
if ($response->getStatusCode() === 200 && $request->path() !== 'health') {
Log::info('Request served', [
'path' => $request->path(),
'duration_ms' => round((microtime(true) - LARAVEL_START) * 1000),
]);
}
}Think of it as an afterCommit for the response: logging, resource cleanup, or notifications that are not worth making the user wait for.
Three Useful Middleware for a Laravel 13 Blog
Locale Detection from the First URL Segment
A bilingual blog like blenderdeluxe needs to know which language the visitor is asking for before rendering the view. A custom middleware can read the first URL segment and set the app locale:
public function handle(Request $request, Closure $next): Response
{
$locale = $request->segment(1);
if (in_array($locale, ['en', 'es'], true)) {
app()->setLocale($locale);
}
return $next($request);
}Before, that check was repeated in every controller; with middleware, the cross-cutting logic lives in a single place.
Security Headers and Controlled CORS
Another perfect candidate is a global middleware that adds security headers to every response:
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
return $response;
}And if you expose an API, the CORS middleware Laravel applies to the api group by default is a great example of how the framework solves with middleware what used to be server configuration.
Slow-Request Logging and Your Own Maintenance Mode
The same pattern covers performance: a middleware that times the request and logs anything above a threshold, or a custom maintenance mode that answers 503 with a personalized view during deploys. All of them are variations of the same idea: intercept, decide, and either let the request pass or respond.
Execution Order and Best Practices
How the Stack Is Ordered and Why It Matters
Stack order is execution order: a request enters through the first middleware and exits through the last, and the response makes the reverse trip. That is why the order of append/prepend and of the groups matters: an auth middleware must run before one that assumes the user exists. Picture it as an onion: the request crosses each layer inward and the response comes back outward.
Common Mistakes: Unregistered Aliases, withoutMiddleware, and Globals
The most common failure is using an alias you never registered in withMiddleware(): Laravel throws a class-not-found exception. The second classic is trying to remove a global middleware with withoutMiddleware(): that method only removes middleware from specific routes, never globals, so calling it on them has no effect. Name your middleware by intent (Ensure..., Set..., Log...), register short aliases, and do not overuse globals: each one runs on every request, and an enormous stack ends up costing a lot to maintain.
Conclusion
Middleware in Laravel 13 is the natural home for all cross-cutting logic: authentication, locale, security, and logging, without dirtying your controllers. Create them with make:middleware, register them in bootstrap/app.php with withMiddleware(), and assign them by alias to routes or groups. If you want the full authentication flow, check out the social login guide with Socialite or the WebAuthn passkeys guide. And keep reading the blog: a new Laravel 13 web development guide lands every week.

