API Design-First Testing Workflow: Spec → Mock → Implement → Test

API Design-First Testing Workflow: Spec → Mock → Implement → Test

Most teams write code first, then document it. The API exists; you write the OpenAPI spec to describe what you built. This approach has a persistent problem: the code and the documentation diverge. Consumers get specs that don't match the real behavior. Tests get written against the implementation, not the contract.

Design-first flips the order. You write the spec before writing any code. The spec becomes the source of truth that drives mocks, client generation, contract tests, and implementation. By the time you ship, every layer has been validated against the same definition.

The Four Phases

Phase 1: Spec (Design)

Before writing a line of implementation code, write the OpenAPI spec. This forces design decisions upfront: what are the resource names, what do request and response bodies look like, what status codes do you return, how do you handle errors?

Start with the highest-priority endpoints. You don't need to spec the entire API before starting development — spec one feature at a time.

openapi: 3.1.0
info:
  title: Orders API
  version: "1.0"
paths:
  /orders:
    post:
      operationId: createOrder
      summary: Create a new order
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrderRequest"
            example:
              customerId: "cust_01HQKF5P2V"
              items:
                - productId: "prod_01HQM"
                  quantity: 2
                  unitPrice: 2999
              currency: "USD"
      responses:
        "201":
          description: Order created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "422":
          description: Invalid request data
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetail"

Review the spec before moving forward. This is your design review. Would you be happy consuming this API? Are the field names consistent with other APIs in your system? Does the error format match your conventions?

Lint the spec with Spectral:

spectral lint openapi.yaml

Fix any violations. A clean lint pass means the spec is well-formed and ready for the next phase.

Phase 2: Mock (Build Against the Spec)

With a validated spec, spin up a mock server with Prism:

prism mock openapi.yaml

Now your frontend team, mobile team, and any other consumers can start building against the API immediately — before the backend is written. They don't need to wait. They have a spec-compliant server running at http://localhost:4010.

Consumers test their integration against the mock:

const ordersApi = new OrdersApi({
  basePath: process.env.API_URL || "http://localhost:4010",
});

// Works immediately — mock returns spec-compliant responses
const order = await ordersApi.createOrder({
  customerId: "cust_01HQKF5P2V",
  items: [{ productId: "prod_01HQM", quantity: 2, unitPrice: 2999 }],
  currency: "USD",
});

Use Prefer headers in integration tests to cover edge cases the spec defines:

// Test the error case
const response = await fetch("http://localhost:4010/orders", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Prefer": "code=422",
  },
  body: JSON.stringify({ items: [] }),
});

expect(response.status).toBe(422);

Frontend teams ship integration code against the mock. These tests run in CI against the mock server. When the backend ships, you swap the URL — the tests run against the real server.

Phase 3: Implement (Code Against the Contract)

Now write the backend. The spec is the spec; your job is to make the server match it.

Run Schemathesis against your implementation as you develop:

st run openapi.yaml --url http://localhost:8000 --checks all

Early in development, Schemathesis will find failures. That's the point. It's testing that your implementation actually matches the contract you designed:

FAILURE: POST /orders
Input: {"customerId": "", "items": [], "currency": "USD"}
Response: 500 Internal Server Error

Expected: 422 (per spec), or any non-500 response

This tells you: the server crashes on an empty items array instead of returning a 422. Fix the implementation until Schemathesis passes.

Phase 4: Test (Validate the Contract)

With implementation complete, run the full contract test suite:

Dredd — deterministic smoke test:

dredd openapi.yaml http://localhost:8000

Every example in the spec is sent to the server. Every response is validated against the schema. This is your regression guard — run it on every commit.

Schemathesis — property-based coverage:

st run openapi.yaml --url http://localhost:8000 \
  --stateful=links \
  --hypothesis-max-examples 200

Property tests explore the input space your examples don't cover. Run this in CI on pull requests; run with higher example counts overnight.

Generated client tests — typed integration:

openapi-generator-cli generate -i openapi.yaml -g typescript-axios -o ./client
cd client && npm <span class="hljs-built_in">test

Type-safe tests verify business logic on top of the contract layer.

CI Pipeline: All Four Layers

name: API Tests
on: [pull_request]
jobs:
  lint:
    name: Spec Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @stoplight/spectral-cli
      - run: spectral lint openapi.yaml --fail-severity error

  contract:
    name: Contract Tests
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - name: Start API server
        run: docker-compose up -d api
      - name: Dredd smoke test
        run: |
          npm install -g dredd
          dredd openapi.yaml http://localhost:8000
      - name: Schemathesis
        uses: schemathesis/action@v1
        with:
          schema: "http://localhost:8000/openapi.yaml"
          args: "--checks all"

  integration:
    name: Integration Tests
    runs-on: ubuntu-latest
    needs: contract
    steps:
      - uses: actions/checkout@v4
      - name: Start API server
        run: docker-compose up -d api
      - name: Run typed client tests
        run: npm test
        env:
          API_URL: http://localhost:8000

What Changes Compared to Code-First

Code-First Design-First
When design decisions get reviewed After implementation Before implementation
When consumers can start building After backend ships After spec is written
When contract drift is caught In production In CI
API consistency Per-developer Enforced by spec
Change communication Changelog emails Spec diff in PR

Managing Spec Changes

The spec is code. Treat it like code: version control, code review, automated validation.

When an endpoint changes:

  1. Update the spec
  2. Run spectral lint — catches spec violations
  3. Run prism mock — consumers can verify their integration against the new spec
  4. Update the implementation
  5. Run Dredd + Schemathesis — confirms the implementation matches the updated spec

Consumers review spec diffs in pull requests. They know what's changing before it ships. They update their integration code against the updated mock before the backend deploys. By the time the change reaches production, every consumer has already validated against it.

Getting Started

You don't need to adopt every tool at once. Start with:

  1. Spectral — lint your existing spec (you may not have one yet; start with the most-used endpoint)
  2. Prism — give your frontend team a mock server immediately
  3. Dredd — add a smoke test to CI in an afternoon

Add Schemathesis and generated clients once the foundation is solid. The design-first workflow is incremental — you can add each layer independently.

For teams that also need behavioral coverage — verifying user flows that span multiple API calls and frontend state — HelpMeTest provides end-to-end test automation that works alongside spec-driven contract tests. Design-first gives you a solid API contract; end-to-end tests verify that the product built on top of that contract works for users.

Read more