Why a Contract‑First Approach Matters
When a startup or agency builds a public API, the first temptation is to start coding the endpoints and document them later. That works for a quick prototype, but as the product scales the lack of a shared contract becomes a hidden cost:
- Ambiguous expectations: Front‑end teams, mobile developers, or third‑party partners guess request shapes and error codes, leading to mismatched implementations.
- Hard‑to‑maintain versioning: Without a single source of truth, any change ripples through the codebase and documentation, often causing silent breakages.
- Testing gaps: Unit tests verify business logic, but they rarely confirm that the API conforms to the public contract.
A contract‑first workflow flips the order: you define the API contract in an OpenAPI (Swagger) document first, then generate validation, scaffolding, and tests from that spec. The result is a single, version‑controlled artifact that both developers and consumers can rely on.
Defining the Contract with OpenAPI
Laravel ships with no built‑in OpenAPI generator, but the community provides solid tools. The most straightforward path is to write a openapi.yaml file and keep it under version control.
openapi: 3.0.3
info:
title: Project Management API
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/projects:
get:
summary: List projects
tags:
- Projects
parameters:
- in: query
name: page
schema:
type: integer
default: 1
- in: query
name: per_page
schema:
type: integer
default: 20
responses:
'200':
description: A paginated list of projects
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectCollection'
post:
summary: Create a new project
tags:
- Projects
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewProject'
responses:
'201':
description: Project created
content:
application/json:
schema:
$ref: '#/components/schemas/Project'
components:
schemas:
NewProject:
type: object
required:
- name
properties:
name:
type: string
maxLength: 150
description:
type: string
maxLength: 500
Project:
allOf:
- $ref: '#/components/schemas/NewProject'
- type: object
properties:
id:
type: integer
created_at:
type: string
format: date-time
ProjectCollection:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Project'
meta:
type: object
properties:
current_page:
type: integer
per_page:
type: integer
total:
type: integer
The spec above captures two endpoints, pagination, request validation, and response shapes. Keep this file in docs/openapi.yaml. When you need to add a field or a new route, you edit the spec first, run the generators, then implement the controller logic.
Generating Request Validation from the Spec
Laravel's Form Request classes are perfect for declarative validation. The goldspecdigital/laravel-openapi package can parse an OpenAPI document and generate a Form Request for each operation.
composer require goldspecdigital/laravel-openapi
php artisan openapi:generate-request "#/paths/~1projects/post"
The command creates app/Http/Requests/StoreProjectRequest.php with rules derived from the NewProject schema:
<?php
namespace AppHttpRequests;
use IlluminateFoundationHttpFormRequest;
class StoreProjectRequest extends FormRequest
{
public function authorize(): bool
{
// Authorization can be tied to Sanctum scopes later
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:150'],
'description' => ['nullable', 'string', 'max:500'],
];
}
}
Because the request class is generated from the spec, the validation logic cannot drift. If the spec changes, re‑run the generator and the compiler will warn you of any missing or mismatched fields.
Securing the API – Authentication, Scopes, and Rate Limits
Security is non‑negotiable for any production API. Laravel Sanctum provides a lightweight token‑based system that works well for SPAs, mobile apps, and third‑party integrations.
1. Token‑Based Authentication
Add Sanctum to the project:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
In config/sanctum.php set stateful domains for your SPA, and enable guard for API routes.
2. Scope‑Driven Access Control
OpenAPI lets you describe required security scopes per operation. Extend the spec:
security:
- bearerAuth: []
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
paths:
/projects:
post:
security:
- bearerAuth: [create:project]
Now, in the generated Form Request you can hook into authorize() to check the token’s scopes:
public function authorize(): bool
{
return $this->user()->tokenCan('create:project');
}
3. Rate Limiting
Laravel’s ThrottleRequests middleware works with route groups. Define a sensible limit that reflects your business model:
Route::middleware([
'auth:sanctum',
'throttle:60,1', // 60 requests per minute per user
])->group(function () {
Route::apiResource('projects', ProjectController::class);
});
For public endpoints (e.g., a public catalog) you can apply a stricter IP‑based limit:
Route::middleware('throttle:30,1')->get('catalog', CatalogController::class);
Automated Testing Against the Contract
A contract‑first workflow shines when you can verify that the implementation matches the OpenAPI definition. Two tools are especially useful:
- Dredd – a language‑agnostic test runner that reads an OpenAPI spec and issues real HTTP requests.
- phpunit‑openapi – a PHPUnit extension that validates responses against a schema.
1. Install Dredd (Node.js)
npm install -g dredd
Create a simple Dredd configuration dredd.yml:
swagger: docs/openapi.yaml
endpoint: http://localhost:8000
hooks: tests/DreddHooks.php
2. Write a Hook to Insert Test Data
Dredd runs against a live server, so you need known data. In tests/DreddHooks.php:
<?php
use Laravel\Dusk\Browser;
$hooks = function ($hooks) {
$hooks->beforeAll(function () {
// Migrate and seed a fresh database for the test run
exec('php artisan migrate:fresh --seed');
});
};
return $hooks;
Run the tests:
php artisan serve &
dredd
Dredd will report any mismatches in request/response shape, status codes, or missing fields. Because the spec lives in source control, failing tests become a gate that prevents accidental contract violations.
3. PHPUnit Schema Assertions
For unit‑level confidence, add a PHPUnit test that validates a single controller response:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\Project;
class ProjectApiTest extends TestCase
{
use RefreshDatabase;
public function test_create_project_returns_valid_schema()
{
$user = User::factory()->create();
$token = $user->createToken('api', ['create:project'])->plainTextToken;
$payload = ['name' => 'New Project', 'description' => 'Demo'];
$response = $this->withHeaders([
'Authorization' => "Bearer $token",
'Accept' => 'application/json',
])->postJson('/api/projects', $payload);
$response->assertStatus(201);
$response->assertJsonStructure([
'id', 'name', 'description', 'created_at',
]);
}
}
While this test does not check the full OpenAPI schema, it complements Dredd by catching regressions early in the CI pipeline.
Deployment Checklist and Observability
Even the most secure API can become a liability if you cannot see what’s happening in production. Add these lightweight steps to your CI/CD pipeline:
- Schema Validation on Deploy – Run
php artisan openapi:validate(provided by the same package) to ensure the spec is syntactically correct. - Static Analysis – Use
phpstanorpsalmto catch type mismatches that could break generated request classes. - Health Endpoint – Expose
/api/healththat returns the current API version and a short OpenAPI checksum. Monitoring tools can ping this endpoint to confirm the service is alive. - Structured Logging – Log each request with
request_id,user_id,route, and responsestatus. Laravel’s built‑inLog::channel('stack')can be configured to output JSON for log aggregators. - Metrics – Increment Prometheus counters for
api_requests_totalwith labelsmethod,endpoint, andstatus. Thespatie/laravel-metricspackage makes this trivial. - Tracing – If you already use OpenTelemetry for other services, add the
elastic/otel-laravelpackage to propagate trace IDs across microservices.
By automating these checks, you reduce the risk of a broken contract slipping into production and give your ops team the visibility they need to act quickly.
Takeaway: A Practical Workflow
- Write or update the OpenAPI spec before any code change.
- Generate Form Requests and, optionally, controller stubs from the spec.
- Implement business logic inside the generated controllers, keeping them thin.
- Secure endpoints using Sanctum tokens and scope checks defined in the spec.
- Run Dredd (or similar) in CI to guarantee the implementation matches the contract.
- Deploy with automated validation, logging, and metrics to keep the API observable.
Following this contract‑first, security‑first pipeline means your startup can ship APIs faster without sacrificing reliability. The same process scales from a single‑developer prototype to a multi‑team microservice ecosystem, giving founders and technical leads confidence that the API they expose will behave exactly as documented.
- Laravel
- Api
- Security
- Testing
- Openapi