Laravel Magazine

5 Laravel Middleware Tricks You Probably Are Not Using

Eric Van Johnson · Tips
5 Laravel Middleware Tricks You Probably Are Not Using

Most Laravel developers use middleware for authentication and maybe a CORS header or two, and then leave the rest of the feature set untouched. That is leaving a lot on the table. Here are five middleware patterns worth knowing.

1. Terminable Middleware — Run Code After the Response Is Sent

Normal middleware runs before or after the controller, but still within the request/response cycle. Terminable middleware runs after the response has already been sent to the browser. This is ideal for expensive operations that the user should not have to wait for — logging, analytics, session writes, and cache warming.

class RecordPageView implements TerminableMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    public function terminate(Request $request, Response $response): void
    {
        // Runs after the response is sent — user never waits for this
        PageView::record(
            url: $request->fullUrl(),
            statusCode: $response->getStatusCode(),
            userId: $request->user()?->id,
            duration: defined('LARAVEL_START') ? microtime(true) - LARAVEL_START : null,
        );
    }
}

Register it in bootstrap/app.php like any other middleware. The framework detects the TerminableMiddleware interface and calls terminate() automatically after the response is flushed.

Note: this works cleanly with FPM. With Octane (Swoole/RoadRunner), the process does not exit after a request, so "after response" timing behaves slightly differently — check the Octane docs for the nuances.

2. Middleware Priority — Control the Order They Run

By default, middleware registered in a group runs in the order they were added. But some middleware has hard dependencies on others. Authentication must run before authorization. Session must be started before CSRF verification reads from it. When the automatic order breaks things, you can declare an explicit priority.

In bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->priority([
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
        \Illuminate\Routing\Middleware\ThrottleRequests::class,
        \App\Http\Middleware\EnsureEmailIsVerified::class,
    ]);
})

Middleware in the priority list always runs before middleware not in the list. Within the priority list, order is top-to-bottom. This is the right tool when you have middleware that needs to set something up before another middleware can use it.

3. Middleware Parameters — Pass Configuration at the Route Level

Middleware can accept parameters, which lets you write a single middleware class that behaves differently based on what the route passes to it. The classic example is role checking:

class RequireRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (! $request->user()->hasAnyRole($roles)) {
            abort(403);
        }

        return $next($request);
    }
}

Apply it in routes using a colon separator for the middleware name and comma-separated values for multiple roles:

Route::get('/admin/users', AdminUserController::class)
    ->middleware('role:admin,super-admin');

Route::get('/reports', ReportController::class)
    ->middleware('role:analyst');

This is far cleaner than writing a separate middleware class per role or duplicating the auth logic across controllers.

4. Scoped Route Groups — Apply Middleware to Nested Resources

When building modular apps with Filament panels, tenant-aware systems, or API versioning, you sometimes need a chunk of routes that all share middleware but are logically separate from the rest of your application. scopeBindings() combined with grouped middleware makes this tidy:

Route::prefix('api/v2')
    ->middleware(['api', 'auth:sanctum', 'verified'])
    ->name('api.v2.')
    ->group(function () {
        Route::apiResource('orders', V2\OrderController::class);
        Route::apiResource('invoices', V2\InvoiceController::class);

        Route::prefix('admin')
            ->middleware('role:admin')
            ->name('admin.')
            ->group(function () {
                Route::apiResource('users', V2\Admin\UserController::class);
            });
    });

The inner role:admin middleware only applies to the nested admin group. The outer authentication middleware applies to everything. You get a clear, readable hierarchy of requirements without repeating middleware on individual routes.

5. withoutMiddleware() — Exclude Specific Middleware on Individual Routes

Sometimes you need one route inside a group to skip middleware that applies to all the others. The escape hatch is withoutMiddleware():

Route::middleware(['auth', 'verified', 'throttle:api'])->group(function () {
    Route::get('/profile', ProfileController::class);
    Route::get('/settings', SettingsController::class);

    // This endpoint is called by webhooks — no auth, different throttle
    Route::post('/webhooks/stripe', StripeWebhookController::class)
        ->withoutMiddleware(['auth', 'verified'])
        ->middleware('throttle:webhooks');
});

You can also use it globally to exclude a middleware from all routes:

->withMiddleware(function (Middleware $middleware) {
    $middleware->except([
        \App\Http\Middleware\EnsureEmailIsVerified::class => [
            StripeWebhookController::class,
        ],
    ]);
})

This is cleaner than the alternative of moving the route outside the group and re-applying all the other middleware manually.

Middleware has a lot more surface area than most developers explore. These five patterns cover the cases that come up most often in real applications, and knowing they exist saves a lot of time that would otherwise go into awkward workarounds.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.