Mastering Middleware in Laravel 12


Laravel's middleware provides a convenient mechanism for filtering HTTP requests entering your application. From authentication to CSRF protection, middleware serves as the guardians of your application, ensuring requests meet specific criteria before reaching your application logic.
In Laravel 12, middleware execution order can be critical to your application functioning correctly. This article dives deep into managing middleware execution order, middleware parameters, terminable middleware, and manually managing Laravel's global middleware stack.
Understanding Middleware Execution Order
Middleware in Laravel executes in a specific sequence based on how they're registered. By default, middleware runs in the order they're added to a route:
Route::get('/profile', function () {
// Your route logic
})->middleware(['auth', 'verified', 'log-activity']);
In this example, the auth
middleware runs first, followed by verified
, and finally log-activity
. But what happens when you need a different order?
Sorting Middleware in Laravel 12
Sometimes, you need middleware to execute in a specific order regardless of how they're assigned to routes. Laravel 12 provides a powerful solution through the priority
method in your application's bootstrap/app.php
file:
->withMiddleware(function (Middleware $middleware) {
$middleware->priority([
\App\Http\Middleware\ImportantFirstMiddleware::class,
\App\Http\Middleware\MustRunSecondMiddleware::class,
\App\Http\Middleware\ThirdInLineMiddleware::class,
// Other middleware in order of priority
]);
})
When to Use Middleware Priority?
You should use middleware priority when:
Dependency Between Middleware: One middleware depends on another being executed first
Global Order Requirements: Your application requires a consistent execution order across all routes
Complex Middleware Chains: You have many middleware that need to interact in specific ways
Real-World Example
Let's say you're building a multi-tenant application with middleware that handles various aspects:
IdentifyTenant
middleware identifies which tenant the request belongs toSetupTenantDatabase
middleware switches to the appropriate database connectionApplyTenantTheme
middleware adjusts UI elements based on tenant settings
In this case, IdentifyTenant
must run before SetupTenantDatabase
, which must run before ApplyTenantTheme
. You can ensure this with:
->withMiddleware(function (Middleware $middleware) {
$middleware->priority([
\App\Http\Middleware\IdentifyTenant::class,
\App\Http\Middleware\SetupTenantDatabase::class,
\App\Http\Middleware\ApplyTenantTheme::class,
// Other middleware
]);
})
What Happens Without Proper Sorting?
Without proper middleware sorting:
Broken Functionality: Features dependent on middleware execution order may fail
Data Inconsistencies: Data transformations might be applied incorrectly
Security Vulnerabilities: Authentication or authorization checks might run after they're needed
Performance Issues: Performance monitoring or caching might be ineffective
Middleware Parameters
Laravel 12 also allows middleware to receive additional parameters, making them more versatile.
How to Define Parametrized Middleware
Let's create a middleware that restricts access based on user subscription level:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RequireSubscription
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next, string $level): Response
{
$user = $request->user();
if (!$user || !$user->hasSubscription($level)) {
return response()->json([
'error' => 'This feature requires a ' . ucfirst($level) . ' subscription',
'upgrade_url' => route('subscriptions.upgrade')
], 403);
}
return $next($request);
}
}
How to Use Parametrized Middleware
You can pass parameters to middleware when defining routes:
Route::get('/premium-content', function () {
// Premium content logic
})->middleware('subscription:premium');
Or using the class-based syntax:
use App\Http\Middleware\RequireSubscription;
Route::get('/analytics-dashboard', function () {
// Analytics dashboard logic
})->middleware(RequireSubscription::class.':business');
For multiple parameters, separate them with commas:
Route::get('/developer-tools', function () {
// Developer tools logic
})->middleware('subscription:premium,business');
Real-World Use Case for Middleware Parameters
Consider a content filtering middleware that adjusts what users can see based on age restrictions:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ContentFilter
{
public function handle(Request $request, Closure $next, int $minimumAge = 13, bool $skipWarning = false): Response
{
$user = $request->user();
$userAge = $user ? $user->age : null;
// Guest users or underage users
if (!$userAge || $userAge < $minimumAge) {
if ($skipWarning) {
return redirect()->route('content.restricted');
}
// Store original URL for after confirmation
session(['intended_url' => $request->fullUrl()]);
return redirect()->route('content.warning', [
'minimum_age' => $minimumAge
]);
}
return $next($request);
}
}
Usage:
// Default age restriction (13+)
Route::get('/forums', function () {
// Forum content
})->middleware('content.filter');
// Mature content (18+) with direct block
Route::get('/mature-content', function () {
// Mature content
})->middleware('content.filter:18,true');
// Teen content (16+) with warning
Route::get('/teen-content', function () {
// Teen content
})->middleware('content.filter:16,false');
Terminable Middleware
Sometimes, you need middleware to perform actions after the HTTP response has been sent to the browser. Laravel 12 supports this through terminable middleware.
How to Create Terminable Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
class TrackPageViews
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
// Pass through to controller action
return $next($request);
}
/**
* Handle tasks after the response has been sent to the browser.
*/
public function terminate(Request $request, Response $response): void
{
// Skip AJAX requests and non-successful responses
if ($request->ajax() || $response->getStatusCode() !== 200) {
return;
}
// Only track GET requests to page views
if ($request->method() !== 'GET') {
return;
}
// Record the page view asynchronously after response is sent
$path = $request->path();
$userAgent = $request->userAgent();
$userId = $request->user()?->id;
DB::table('page_views')->insert([
'path' => $path,
'user_id' => $userId,
'user_agent' => $userAgent,
'ip_address' => $request->ip(),
'viewed_at' => now(),
]);
}
}
When to Use Terminable Middleware?
Terminable middleware is perfect for:
Logging: Recording request/response data after the response is sent
Queue Jobs: Dispatching background jobs based on request data
Cleanup: Performing cleanup operations after request processing
Performance Tracking: Measuring and recording performance metrics
Important Note on Terminable Middleware
By default, Laravel creates a fresh instance of middleware for the terminate
method. If you want to use the same instance for both handle
and terminate
, register the middleware as a singleton:
// In AppServiceProvider.php
public function register(): void
{
$this->app->singleton(\App\Http\Middleware\LogResponseTime::class);
}
Manually Managing Laravel's Default Global Middleware
Laravel 12 offers enhanced control over the global middleware stack through the bootstrap/app.php
file.
Customizing the Global Middleware Stack
->withMiddleware(function (Middleware $middleware) {
$middleware->use([
\Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class,
\Illuminate\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Http\Middleware\ValidatePostSize::class,
\Illuminate\Foundation\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
// Your custom global middleware here
]);
})
Replacing Default Middleware
You can replace Laravel's default middleware with your own custom implementations:
use App\Http\Middleware\CustomSessionHandler;
use Illuminate\Session\Middleware\StartSession;
$middleware->web(replace: [
StartSession::class => CustomSessionHandler::class,
]);
Removing Default Middleware
If a default middleware doesn't suit your needs, you can remove it entirely:
$middleware->web(remove: [
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
]);
Best Practices for Laravel 12 Middleware Management
Keep a Clear Order: Maintain a clear and documented middleware execution order
Create Single-Purpose Middleware: Each middleware should have one clear responsibility
Use Group-Specific Middleware: Use middleware groups for route-specific middleware
Optimize Performance: Be mindful of middleware overhead, especially on high-traffic routes
Handle Errors Appropriately: Middleware should handle exceptions gracefully
Test Middleware Chains: Thoroughly test how your middleware interacts in chains
Conclusion
Understanding and controlling middleware execution order in Laravel 12 is crucial for building robust applications. Whether you're handling authentication, localization, or custom business logic, properly sorting middleware ensures your application behaves predictably and securely.
The ability to define execution priority, pass parameters, and execute code after sending responses makes Laravel middleware a powerful tool in your development arsenal. By mastering these concepts, you'll create more maintainable, secure, and efficient Laravel applications.
Remember that middleware is part of your application's foundation - investing time in properly organizing it pays dividends in the long run through reduced bugs, better security, and more maintainable code.
What's your experience with Laravel middleware? Have you encountered situations where middleware execution order was critical? Share your thoughts in the comments below!
Subscribe to my newsletter
Read articles from Asfia Aiman directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Asfia Aiman
Asfia Aiman
Hey Hashnode community! I'm Asfia Aiman, a seasoned web developer with three years of experience. My expertise includes HTML, CSS, JavaScript, jQuery, AJAX for front-end, PHP, Bootstrap, Laravel for back-end, and MySQL for databases. I prioritize client satisfaction, delivering tailor-made solutions with a focus on quality. Currently expanding my skills with Vue.js. Let's connect and explore how I can bring my passion and experience to your projects! Reach out to discuss collaborations or learn more about my skills. Excited to build something amazing together! If you like my blogs, buy me a coffee here https://www.buymeacoffee.com/asfiaaiman