Skip to main content
Mohamed

How Middleware Works in Laravel: The Bouncer at Your App's Door

Learn how Laravel middleware works with practical examples and real code. From basic auth to custom API protection - explained by a developer who's been there.

So here's the thing - I've been working with Laravel for a couple of years now, and I still remember the first time I encountered middleware. I was following some tutorial, copy-pasting code like we all do, and suddenly there was this ->middleware('auth') thing attached to my routes. It worked, but I had absolutely no clue why.

Fast forward to today, and middleware is probably one of my favorite Laravel features. It's like having a really smart bouncer at your app's front door who knows exactly what to do with every visitor.

What is This Middleware Thing Anyway?

Okay, let's keep this simple. Middleware is basically code that runs before your controller gets the request. Think of it like airport security - you can't just walk onto the plane, right? You gotta go through multiple checkpoints first.

Your HTTP request goes through the same process:

  1. Someone visits your website
  2. Middleware #1: "Are you a real person or a bot?"
  3. Middleware #2: "Are you logged in?"
  4. Middleware #3: "Do you have permission to be here?"
  5. Finally: Your controller does its job

I like to think of it as having multiple filters on your Instagram story, but for web requests.

Laravel's Free Middleware (The Good Stuff)

Laravel ships with some really useful middleware out of the box. You've probably used some without even knowing:

  • auth - The classic "you shall not pass" for logged-out users
  • guest - Kicks out logged-in users (useful for login pages)
  • throttle - Stops people from spamming your API
  • verified - Makes sure users confirmed their email

I remember spending hours trying to build my own authentication system before discovering these existed. Don't be like past me - use what Laravel gives you!

Building Your First Middleware

Let me show you how to create middleware that actually does something useful. I was debugging an API issue last month and needed to log every request hitting a specific endpoint. Here's what I built:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class RequestLogger
{
    public function handle(Request $request, Closure $next)
    {
        // Log what's coming in
        Log::info('New request coming in', [
            'url' => $request->fullUrl(),
            'method' => $request->method(),
            'ip' => $request->ip(),
            'user_agent' => $request->userAgent()
        ]);
        
        // Let the request continue
        $response = $next($request);
        
        // Log what we're sending back
        Log::info('Response sent', [
            'status' => $response->getStatusCode(),
            'size' => strlen($response->getContent())
        ]);
        
        return $response;
    }
}

To create this, just run:

php artisan make:middleware RequestLogger

The artisan command creates the file structure for you. Pretty neat, right?

Now you can use it in your routes:

Route::get('/api/users', [UserController::class, 'index'])
    ->middleware('log.requests');

// Or on a group of routes
Route::middleware(['log.requests', 'auth'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/profile', [ProfileController::class, 'show']);
});

The Order Actually Matters (I Learned This The Hard Way)

Here's something that bit me early on - middleware runs in the order you specify. I once put auth middleware after a middleware that required user data, and spent two hours wondering why everything was broken.

// This makes sense
Route::middleware(['throttle:60,1', 'auth', 'verified'])
    ->get('/admin', [AdminController::class, 'index']);

// This doesn't - you're checking for user verification before checking if they're logged in
Route::middleware(['verified', 'auth'])
    ->get('/admin', [AdminController::class, 'index']);

Think of it like getting dressed - you put on your underwear before your pants, not the other way around.

A Real Example: API Key Protection

I built this for a client project where we needed to protect API endpoints with simple key authentication:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class CheckApiKey
{
    public function handle(Request $request, Closure $next)
    {
        $apiKey = $request->header('X-API-KEY');
        
        // No key provided
        if (!$apiKey) {
            return response()->json([
                'message' => 'API key required'
            ], 401);
        }
        
        // Wrong key
        if ($apiKey !== config('services.api.key')) {
            return response()->json([
                'message' => 'Invalid API key'
            ], 403);
        }
        
        // All good, let them through
        return $next($request);
    }
}

Then I protect my API routes like this:

Route::middleware('api.key')->prefix('api')->group(function () {
    Route::get('/users', [UserController::class, 'index']);
    Route::post('/users', [UserController::class, 'store']);
});

Works like a charm, and now I can easily add or remove API key protection from any route.

Global vs Specific Middleware

Some middleware should run on every request (like CORS handling), others only when you need them.

Global middleware goes in the $middleware array in Kernel.php:

protected $middleware = [
    \App\Http\Middleware\TrustProxies::class,
    \Fruitcake\Cors\HandleCors::class,
    // These run on EVERY request
];

Route middleware you apply manually:

Route::get('/admin')->middleware('auth');

I usually keep global middleware to a minimum - only stuff that absolutely needs to run everywhere.

Passing Parameters to Middleware

This is where middleware gets really flexible. You can pass parameters to customize behavior:

public function handle(Request $request, Closure $next, $role)
{
    if (!auth()->check()) {
        return redirect('/login');
    }
    
    if (!auth()->user()->hasRole($role)) {
        abort(403, "You need {$role} access for this page");
    }
    
    return $next($request);
}

Then use it like:

Route::get('/admin')->middleware('role:admin');
Route::get('/manager')->middleware('role:manager');

Same middleware, different behavior based on the parameter. Pretty cool, right?

Mistakes I Made (So You Don't Have To)

  1. Forgetting to return $next($request) - Your app will just hang there like a broken elevator
  2. Putting middleware in the wrong order - Spent half a day debugging this once
  3. Doing heavy database queries in middleware - Keep it fast, middleware runs on every matching request
  4. Not handling edge cases - Always check if variables exist before using them

When Things Break (Debugging Tips)

I add temporary logging when middleware isn't behaving:

public function handle(Request $request, Closure $next)
{
    \Log::debug('Running middleware: ' . self::class);
    \Log::debug('Request data:', $request->all());
    
    $response = $next($request);
    
    \Log::debug('Middleware finished, status: ' . $response->getStatusCode());
    
    return $response;
}

Check your logs in storage/logs/laravel.log and you'll see exactly what's happening.

Wrapping This Up

Middleware seemed scary when I started, but it's actually one of the most straightforward concepts in Laravel. It's just code that runs between the request and your controller - like a security checkpoint at the airport.

The best part? Once you build a middleware, you can reuse it across your entire app. I have a collection of middleware I copy between projects now - logging, API authentication, rate limiting, you name it.

Start simple, maybe with a logging middleware like I showed you. Then gradually build more complex ones as you need them. Before you know it, you'll be thinking in middleware for every new feature.


Been wrestling with Laravel authentication or performance issues? I write about this stuff regularly and love helping fellow developers figure it out. Hit me up if you're stuck on something!

Mohamed BELALIA
Full Stack Developer