Spec-First API Development: Make Your OpenAPI File the Source of Truth
Most teams write the API first and the spec later — if ever. The code ships, the OpenAPI document drifts, and six months on the docs describe an API that no longer exists. Spec-first flips the order: you design the contract in OpenAPI before writing a single route, then generate mocks, validation, docs, and clients from that one file. The spec stops being documentation and becomes the source of truth.
Here's how to actually do it.
1. Design the contract first
Start with a minimal but precise OpenAPI 3.1 document. Describe the shape of requests and responses, not the implementation.
# openapi.yaml
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
paths:
/orders/{id}:
get:
operationId: getOrder
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
"200":
description: The order
content:
application/json:
schema: { $ref: "#/components/schemas/Order" }
"404":
description: Not found
components:
schemas:
Order:
type: object
required: [id, status, total]
properties:
id: { type: string }
status: { type: string, enum: [pending, paid, shipped] }
total: { type: number }
Because this is written before the code, frontend and backend can agree on the contract on day one instead of arguing about field names in code review.
2. Mock the API before it exists
Your frontend team shouldn't wait for the backend. Point a mock server at the spec and they can build against realistic responses immediately. Prism does this with zero code:
npx @stoplight/prism-cli mock openapi.yaml
# GET http://127.0.0.1:4010/orders/123
# → { "id": "string", "status": "pending", "total": 0 }
Prism validates incoming requests against the spec and returns spec-compliant responses. If the frontend sends a malformed request, it fails against the mock — long before it ever hits real infrastructure.
3. Enforce the contract at runtime
The biggest risk with spec-first is drift: the code quietly diverges from the document. Stop that by validating live traffic against the spec. With Express:
const express = require("express");
const OpenApiValidator = require("express-openapi-validator");
const app = express();
app.use(express.json());
app.use(
OpenApiValidator.middleware({
apiSpec: "./openapi.yaml",
validateRequests: true,
validateResponses: true, // catches drift in YOUR responses too
})
);
app.get("/orders/:id", (req, res) => {
res.json({ id: req.params.id, status: "paid", total: 42.0 });
});
// Spec violations become clean 400/500s instead of silent bugs
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ message: err.message });
});
app.listen(3000);
validateResponses: true is the part most people skip — and it's the part that matters. It fails loudly in tests when your handler returns a field the spec doesn't allow, so the code can never silently outrun the contract.
4. Generate clients instead of hand-writing them
Once the spec is authoritative, typed clients are a build step, not a chore:
npx openapi-typescript openapi.yaml -o src/api-types.ts
Now your frontend gets compile-time errors the moment a response shape changes in the spec. No more guessing whether total is a string or a number.
Why this order matters
Spec-first works because every artifact flows from one file: mocks for parallel work, runtime validation to prevent drift, generated types for safety, and always-accurate docs. Change the spec, regenerate everything, and your whole stack stays in sync.
The friction is usually tooling sprawl — a mock server here, a validator there, a docs generator somewhere else, all pointed at copies of a spec that slowly diverge. APIKumo keeps the spec, live versioned docs, mock environments, and generated client code in one workspace, so the contract you design is the contract your team builds against. Define it once, and let everything else follow.
Top comments (0)