Skip to content
Mehedi Hasan Sagor

// blog

Designing Secure, Contract‑First REST APIs in Laravel: A Practical Guide

Mehedi Hasan Sagor Oct 18, 2026 3 min read
Designing Secure, Contract‑First REST APIs in Laravel: A Practical Guide

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:

  1. Schema Validation on Deploy – Run php artisan openapi:validate (provided by the same package) to ensure the spec is syntactically correct.
  2. Static Analysis – Use phpstan or psalm to catch type mismatches that could break generated request classes.
  3. Health Endpoint – Expose /api/health that returns the current API version and a short OpenAPI checksum. Monitoring tools can ping this endpoint to confirm the service is alive.
  4. Structured Logging – Log each request with request_id, user_id, route, and response status. Laravel’s built‑in Log::channel('stack') can be configured to output JSON for log aggregators.
  5. Metrics – Increment Prometheus counters for api_requests_total with labels method, endpoint, and status. The spatie/laravel-metrics package makes this trivial.
  6. Tracing – If you already use OpenTelemetry for other services, add the elastic/otel-laravel package 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

  1. Write or update the OpenAPI spec before any code change.
  2. Generate Form Requests and, optionally, controller stubs from the spec.
  3. Implement business logic inside the generated controllers, keeping them thin.
  4. Secure endpoints using Sanctum tokens and scope checks defined in the spec.
  5. Run Dredd (or similar) in CI to guarantee the implementation matches the contract.
  6. 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