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.yamlFix 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.yamlNow 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 allEarly 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 responseThis 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:8000Every 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 200Property 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">testType-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:8000What 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:
- Update the spec
- Run
spectral lint— catches spec violations - Run
prism mock— consumers can verify their integration against the new spec - Update the implementation
- 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:
- Spectral — lint your existing spec (you may not have one yet; start with the most-used endpoint)
- Prism — give your frontend team a mock server immediately
- 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.