Middleware is Laravel 5's most powerful feature. It provides a convenient mechanism for filtering HTTP requests entering your application, transforming responses leaving it, and everything in between. At ZIRA Software, we use middleware for authentication, logging, CORS, API rate limiting, and dozens of custom use cases.
What is Middleware?
Middleware sits between the request and your application logic:
Request → Middleware → Controller → Middleware → Response
Think of middleware as layers of an onion—each request passes through each layer, and each response passes back through in reverse.
Creating Middleware
php artisan make:middleware CheckAge
This creates app/Http/Middleware/CheckAge.php:
<?php namespace App\Http\Middleware;
use Closure;
class CheckAge {
public function handle($request, Closure $next) {
if ($request->age <= 18) {
return redirect('home');
}
return $next($request);
}
}
Before vs After Middleware
Before Middleware
Runs before the request reaches the controller:
public function handle($request, Closure $next) {
// Do something with request
return $next($request);
}
Use cases:
- Authentication
- Input validation
- Logging
- Rate limiting
After Middleware
Runs after the controller has processed the request:
public function handle($request, Closure $next) {
$response = $next($request);
// Do something with response
return $response;
}
Use cases:
- Response modification
- Adding headers
- Logging responses
- Performance monitoring
Registering Middleware
Global Middleware
Runs on every request:
// app/Http/Kernel.php
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\LogRequests::class,
];
Route Middleware
Assign to specific routes:
// app/Http/Kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
Using Middleware
On Routes
// Single middleware
Route::get('admin/profile', ['middleware' => 'auth', function() {
//
}]);
// Multiple middleware
Route::get('dashboard', ['middleware' => ['auth', 'verified'], function() {
//
}]);
// With parameters
Route::get('api/posts', ['middleware' => 'throttle:60,1', function() {
//
}]);
In Controllers
class UserController extends Controller {
public function __construct() {
$this->middleware('auth');
$this->middleware('admin', ['only' => ['create', 'store', 'destroy']]);
$this->middleware('verified', ['except' => ['index', 'show']]);
}
}
On Route Groups
Route::group(['middleware' => 'auth'], function() {
Route::get('dashboard', 'DashboardController@index');
Route::resource('posts', 'PostController');
Route::resource('comments', 'CommentController');
});
Middleware Parameters
Pass parameters to middleware:
// Define middleware
public function handle($request, Closure $next, $role) {
if (!$request->user()->hasRole($role)) {
abort(403);
}
return $next($request);
}
// Use middleware
Route::get('admin/users', ['middleware' => 'role:admin', function() {
//
}]);
// Multiple parameters
Route::get('posts', ['middleware' => 'throttle:60,1', function() {
// 60 requests per 1 minute
}]);
Real-World Examples
1. API Rate Limiting
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
class ThrottleRequests {
protected $limiter;
public function __construct(RateLimiter $limiter) {
$this->limiter = $limiter;
}
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1) {
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
return $this->buildResponse($key, $maxAttempts);
}
$this->limiter->hit($key, $decayMinutes);
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
protected function resolveRequestSignature($request) {
return sha1($request->user()->id . '|' . $request->ip());
}
protected function buildResponse($key, $maxAttempts) {
$retryAfter = $this->limiter->availableIn($key);
return response('Too Many Attempts.', 429)
->header('Retry-After', $retryAfter)
->header('X-RateLimit-Limit', $maxAttempts)
->header('X-RateLimit-Remaining', 0);
}
protected function addHeaders($response, $maxAttempts, $remainingAttempts) {
$response->headers->add([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
]);
return $response;
}
}
2. Request Logging
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
class LogRequests {
public function handle($request, Closure $next) {
$startTime = microtime(true);
$response = $next($request);
$duration = microtime(true) - $startTime;
Log::info('Request processed', [
'method' => $request->method(),
'url' => $request->fullUrl(),
'ip' => $request->ip(),
'user_id' => $request->user() ? $request->user()->id : null,
'duration' => round($duration * 1000, 2) . 'ms',
'status' => $response->status(),
]);
return $response;
}
}
3. CORS Headers
<?php namespace App\Http\Middleware;
use Closure;
class Cors {
public function handle($request, Closure $next) {
$response = $next($request);
$response->headers->set('Access-Control-Allow-Origin', '*');
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
return $response;
}
}
4. Force HTTPS
<?php namespace App\Http\Middleware;
use Closure;
class ForceHttps {
public function handle($request, Closure $next) {
if (!$request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
}
5. API Authentication Token
<?php namespace App\Http\Middleware;
use Closure;
use App\User;
class AuthenticateWithToken {
public function handle($request, Closure $next) {
$token = $request->header('X-API-TOKEN');
if (!$token) {
return response()->json(['error' => 'Token required'], 401);
}
$user = User::where('api_token', $token)->first();
if (!$user) {
return response()->json(['error' => 'Invalid token'], 401);
}
auth()->login($user);
return $next($request);
}
}
6. Response Time Header
<?php namespace App\Http\Middleware;
use Closure;
class AddResponseTime {
public function handle($request, Closure $next) {
$start = microtime(true);
$response = $next($request);
$duration = microtime(true) - $start;
$response->headers->set('X-Response-Time', round($duration * 1000, 2) . 'ms');
return $response;
}
}
7. Localization
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\App;
class SetLocale {
public function handle($request, Closure $next) {
$locale = $request->header('Accept-Language', 'en');
// Or from user preference
if ($request->user()) {
$locale = $request->user()->locale;
}
App::setLocale($locale);
return $next($request);
}
}
8. Admin Access
<?php namespace App\Http\Middleware;
use Closure;
class AdminAccess {
public function handle($request, Closure $next) {
if (!$request->user() || !$request->user()->isAdmin()) {
if ($request->ajax() || $request->wantsJson()) {
return response()->json(['error' => 'Unauthorized'], 403);
}
return redirect()->route('home')
->with('error', 'You do not have admin access');
}
return $next($request);
}
}
Terminable Middleware
Perform tasks after the response is sent to the browser:
<?php namespace App\Http\Middleware;
use Closure;
class StartSession implements TerminableMiddleware {
public function handle($request, Closure $next) {
return $next($request);
}
public function terminate($request, $response) {
// Save session data after response is sent
session()->save();
// Log analytics
Analytics::log($request, $response);
// Send notifications
NotificationService::process();
}
}
Use cases for terminable middleware:
- Session persistence
- Analytics tracking
- Email notifications
- Cache warming
- Cleanup tasks
Middleware Priority
Laravel processes middleware in this order:
- Global middleware (in order defined)
- Route group middleware
- Route-specific middleware
Control order in $middlewarePriority:
// app/Http/Kernel.php
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
Testing Middleware
class AdminMiddlewareTest extends TestCase {
public function testAdminCanAccessAdminRoutes() {
$admin = factory(User::class)->create(['role' => 'admin']);
$this->actingAs($admin)
->get('/admin/dashboard')
->assertStatus(200);
}
public function testRegularUserCannotAccessAdminRoutes() {
$user = factory(User::class)->create(['role' => 'user']);
$this->actingAs($user)
->get('/admin/dashboard')
->assertStatus(403);
}
public function testGuestIsRedirectedToLogin() {
$this->get('/admin/dashboard')
->assertRedirect('/login');
}
}
Best Practices
- Single Responsibility - Each middleware should do one thing
- Order Matters - Carefully consider middleware execution order
- Performance - Avoid heavy operations in middleware
- Early Returns - Return early when conditions aren't met
- Logging - Log important middleware actions
- Testing - Always test middleware thoroughly
- Documentation - Document parameters and behavior
Common Pitfalls
1. Forgetting to return $next($request):
// Wrong
public function handle($request, Closure $next) {
if ($request->age < 18) {
return redirect('home');
}
// Missing return!
}
// Correct
public function handle($request, Closure $next) {
if ($request->age < 18) {
return redirect('home');
}
return $next($request);
}
2. Modifying request after $next():
// Wrong - too late!
public function handle($request, Closure $next) {
$response = $next($request);
$request->merge(['processed' => true]); // Won't work!
return $response;
}
// Correct
public function handle($request, Closure $next) {
$request->merge(['processed' => true]);
return $next($request);
}
3. Not handling AJAX vs regular requests:
public function handle($request, Closure $next) {
if (!$request->user()->isVerified()) {
if ($request->wantsJson()) {
return response()->json(['error' => 'Email not verified'], 403);
}
return redirect()->route('verification.notice');
}
return $next($request);
}
Advanced Patterns
Middleware Groups
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
],
'api' => [
'throttle:60,1',
'auth:api',
],
];
Conditional Middleware
public function __construct() {
$this->middleware(function ($request, $next) {
if ($request->user() && $request->user()->isBanned()) {
return response('Your account has been banned', 403);
}
return $next($request);
});
}
Conclusion
Middleware is the backbone of Laravel's request-response cycle. It enables clean, reusable code for cross-cutting concerns like authentication, logging, and API management. At ZIRA Software, well-designed middleware has simplified dozens of complex applications.
Master middleware and you'll write cleaner, more maintainable Laravel applications.
Need help architecting complex middleware solutions? Contact ZIRA Software to discuss Laravel architecture, API design, and enterprise application development.