Three topics come up in every discussion of an API’s pipeline. In each, the right amount of code to write is close to none.
CORS. Laravel’s HandleCors middleware is already in the global stack, and its default configuration is open: any origin may call anything under api/*. For a browser-facing API you narrow that to your own front ends. For an API called only by other servers, as this one is, CORS shouldn’t apply at all, because it is a browser mechanism. Publish the config with php artisan config:publish cors and set paths to an empty array. The open default does little harm to an API that authenticates with a bearer token and no cookies, but “does little harm” is not a setting you chose. (Laravel documentation: Routing › Cross-Origin Resource Sharing (CORS).)
Compression. Gzip belongs to the web server or the CDN in front of your application. Nginx compresses a JSON response faster than PHP can, knows which content types to skip, and sets the Vary header correctly.
Transport security. Serve the API over HTTPS only, and refuse plain HTTP outright at the load balancer rather than redirecting it. By the time a redirect is sent, the bearer token has already crossed the network in clear text. Strict-Transport-Security and X-Content-Type-Options: nosniff are one line each in the web server’s configuration.
The Complete Request
POST /api/licenses
server limit by address 429
global proxies, CORS, size limit Laravel
api SetLocale
authentication 401
throttle by consumer 429
ability check 403
request StoreLicenseRequest 422
action LicenseController@store 201
after the request is recorded terminate()
Read the right-hand column from top to bottom. A request is refused as early and as cheaply as possible: an unauthenticated one costs a token lookup, a throttled one costs a cache read, and neither comes near the provider. Arranging that order is what the pipeline is for.
Testing the Pipeline
Middleware is tested through the endpoints it protects:
// tests/Feature/ThrottleTest.php
it('throttles a consumer past its limit', function () {
Sanctum::actingAs(
Consumer::factory()->create(),
['licenses:read'],
);
$this->mock(LicenseContract::class)
->shouldReceive('all')->andReturn(collect());
foreach (range(1, 60) as $attempt) {
$this->getJson('/api/licenses')->assertOk();
}
$this->getJson('/api/licenses')
->assertTooManyRequests()
->assertHeader('Retry-After');
});
The contract is replaced so the test never touches the provider. What is under test is the sixty-first request.
And one for the claim this chapter made about refusals:
// tests/Feature/RequestLogTest.php
it('records a refused request', function () {
$this->getJson('/api/licenses')->assertUnauthorized();
$this->assertDatabaseHas('api_requests', [
'status' => 401,
'consumer_id' => null,
]);
});
Chapter 6 Summary
- Middleware is configured in
bootstrap/app.php. Useapi()for your own, not the global stack. - Read the list of middleware Laravel already runs before writing one.
- Define the
apirate limiter once, keyed by consumer. Laravel sends the rate-limit headers and the 429. - Authentication runs before that limiter, so cap requests by address in front of the application, and answer 429 there too.
- Tell Laravel which proxies to trust, by address, or every anonymous caller shares one counter.
- Set the locale from
Accept-LanguagewithgetPreferredLanguage(). - Record every request in
terminate(), the refused ones included. Never the token. Bodies only when you have decided to. - CORS, compression, and transport security are configuration. Close CORS if no browser calls you, and refuse plain HTTP.