Reader Stacks

Creating Custom Middleware in Laravel 11

Laravel 11 removed app/Http/Kernel.php — here is how middleware registration actually works now, with a working example.

Laravel 11 restructured how middleware is registered. If you're following an older tutorial and looking for app/Http/Kernel.php to add your middleware alias, you won't find it — that file no longer exists in a fresh Laravel 11 or 12 install. Registration now happens in bootstrap/app.php.

1. Generate the middleware

php artisan make:middleware EnsureUserIsSubscribed

This creates app/Http/Middleware/EnsureUserIsSubscribed.php:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserIsSubscribed
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! $request->user()?->subscribed()) {
            return redirect()->route('billing.index');
        }

        return $next($request);
    }
}

2. Register it in bootstrap/app.php

Open bootstrap/app.php and use the withMiddleware() callback:

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
    ]);
})

This is the direct replacement for the old $routeMiddleware array in Kernel.php. The same Middleware object also has append(), prepend(), and group() methods for adding middleware to the global stack or to the web/api groups, replacing the old $middlewareGroups array.

3. Use it on a route

Route::get('/dashboard', DashboardController::class)
    ->middleware('subscribed');

Migrating an existing Kernel.php project

If you're upgrading a Laravel 10 project rather than starting fresh, Laravel 11's upgrade path keeps Kernel.php working if you don't run the structural migration — the new bootstrap/app.php style is the default for new applications, not a forced migration for existing ones. Check which pattern your project actually uses before copying either version of this example.

Middleware should reject or pass the request, not own the feature

The subscription check belongs in middleware because it is a request-access rule. The billing logic that decides whether a user is subscribed should live on a model or service that can be reused elsewhere. Keeping the middleware small matters because it can run on every matching request; database-heavy work hidden in middleware is easy to multiply across a large route group.

Authentication must run before middleware that expects a user

The null-safe call in the example prevents an exception for a guest, but it also redirects every guest to billing. If the real policy is "the user must be logged in and subscribed," compose the existing authentication middleware with the custom one:

Route::get('/dashboard', DashboardController::class)
    ->middleware(['auth', 'subscribed']);

Now auth owns guest handling and subscribed can focus on the narrower rule. Middleware order is observable behavior; do not assume every alias in a list is interchangeable.

Parameters can make one middleware reusable

Middleware can accept arguments after $next, which is useful when the same check has a small, explicit variation:

public function handle(Request $request, Closure $next, string $plan): Response
{
    abort_unless($request->user()->hasPlan($plan), 403);

    return $next($request);
}
->middleware('plan:pro')

Do not push arbitrary business configuration into route strings, but a constrained parameter can avoid several nearly identical middleware classes.

Test both sides of the boundary

A useful feature test proves that an eligible user reaches the route and an ineligible user receives the expected redirect or status. Testing only the handle() method misses alias registration and middleware ordering — the exact integration points that tend to break during an upgrade.

Topics: Authentication & Access Control