Skip to main content
Laravel, shipping fast.
Chapter 4 · The Request Pipeline

Routes and Middleware Ordering

Julian Beaujardin

Protected routes are grouped with middleware in a specific order:

// routes/api.php
Route::get('/health', HealthController::class);  // Public

Route::middleware([
    EnsureTokenIsValidMiddleware::class,
    'throttle:api-token',
    AddRateLimitHeadersMiddleware::class,
    LogApiRequestsMiddleware::class,
])->controller(LicenseController::class)->group(function () {
    Route::get('/licenses', 'show');
    Route::post('/license', 'create');
    Route::delete('/license/{license}', 'destroy');
});

Order matters. Middleware runs top to bottom on the request, bottom to top on the response:

Request path:

  1. EnsureTokenIsValidMiddleware: Validate token exists, not expired, domain matches. If fails, reject with 401. Stop here.
  2. throttle:api-token: Check if token exceeded rate limit. If yes, return 429. Stop here.
  3. AddRateLimitHeadersMiddleware: Add rate limit headers to response.
  4. LogApiRequestsMiddleware: Queue a log job.
  5. Controller: Execute the actual endpoint logic.

Response path:

  1. Response travels back through LogApiRequestsMiddleware (logging happens asynchronously)
  2. Through AddRateLimitHeadersMiddleware (headers already added)
  3. Through throttle middleware (rate limit tracking recorded)
  4. Through EnsureTokenIsValidMiddleware (nothing to do on response)
  5. Back to client

This ordering ensures failed requests stop early. A request with no token never reaches rate limiting. A rate-limited request never reaches your controller. This is efficient and predictable.

Domain Validation: Binding Tokens to Origins

Tokens can restrict which domains are allowed to use them. This is crucial for partner scenarios.

Imagine you issue a token to Partner A. They promise to only use it from partner-a.com. But what if their credentials are stolen? An attacker gets the token but runs from attacker.com. Without domain validation, the token still works. With domain validation, it fails.

Configure token domain restrictions in the factory:

// database/factories/BearerFactory.php
class BearerFactory extends Factory
{
    public function definition(): array
    {
        return [
            'token' => Token::generateToken(),
            'expires_at' => now()->addYear(),
            'domains' => null,  // No domain restrictions by default
            'settings' => [...],
        ];
    }

    public function withDomains(array $domains): self
    {
        return $this->state(fn (array $attributes) => [
            'domains' => $domains,
        ]);
    }
}

// Usage in code or tests:
$token = Bearer::factory()
    ->withDomains(['https://partner-a.com', 'https://partner-a-staging.com'])
    ->create();

Middleware checks domains:

private function isTokenValidForDomain(Request $request, Bearer $token): bool
{
    $domains = $token->domains;

    // No domain config? Always allow
    if (empty($domains) || ! config('bearer.verify_domains', false)) {
        return true;
    }

    // Handle JSON storage edge case
    if (is_string($domains)) {
        $decoded = json_decode($domains, true);
        if (! is_array($decoded)) {
            return false;  // Malformed domains data
        }
        $domains = $decoded;
    }

    // Check if request origin is whitelisted
    return in_array($request->getSchemeAndHttpHost(), $domains, true);
}

getSchemeAndHttpHost() returns the full origin: https://partner-a.com. Checking it against the whitelist ensures the token only works from expected domains.

The Complete Request Flow

Here's what happens when a request arrives:

Client sends: GET /api/licenses
              Header: Authorization: Bearer abc123


Router matches route, checks middleware


EnsureTokenIsValidMiddleware:
    - Extract token "abc123"
    - Cache key: "bearer:abc123"
    - Cache hit? Return cached Bearer model
    - Cache miss? Query database, cache, return
    - Valid? Not expired? Domain matches?
    → If yes, continue
    → If no, return 401


throttle:api-token:
    - Check rate limit for token
    - 60 per minute, 45 remaining?
    → If under limit, continue
    → If over limit, return 429


AddRateLimitHeadersMiddleware:
    - Calculate remaining quota
    - Add headers: X-RateLimit-*


LogApiRequestsMiddleware:
    - Set defer callback to log this request
    - Continue to controller


LicenseServiceProvider::boot():
    - Inspect token settings
    - Set Config to use correct driver
    - Set driver credentials from token


LicenseController::show():
    - Call LicenseFacade::licenses()
    - Facade uses configuration from token
    - Returns LicenseDTO[]
    - Maps to LicenseResource
    - Wraps in CollectionResponse


Response flows back through middleware (bottom to top)


LogApiRequestsMiddleware:
    - Execute deferred callback
    - Queue SendToLogsJob
    - (Happens asynchronously, doesn't block response)


Response sent to client with:
    - Status 200
    - Rate limit headers
    - JSON body


SendToLogsJob executes in queue:
    - Write log entry with request details
    - Token hashed, not exposed
    - Audit trail complete

All of this happens automatically. No controller code has to think about authentication, rate limiting, logging, or configuration. It's all enforced by middleware, all consistent, all proven.