OpenAPI Is Also a Design Artifact
OpenAPI specs are not just documentation to generate after the fact. They are the best tool for designing an API before writing a line of implementation.
An OpenAPI spec is a contract. It says what status codes the client can expect, what error shape errors take, how pagination works, and what fields are required. When you write the spec after the implementation, it is documentation — sometimes accurate, often not. When you write the spec before the implementation, it is a design tool that lets you catch mistakes before they exist in production and generate types before writing a single handler.
The Spec as Prototype
The fastest way to prototype an API without spinning up a server is to write the OpenAPI spec and use it as a mock. Tools like Prism from Stoplight can serve a mock server from any OpenAPI file:
npx @stoplight/prism-cli mock ./openapi.yaml
The frontend team can build against a real HTTP interface with real status codes and example payloads while the backend team implements the actual handlers. The spec is the handshake. Both sides build toward it simultaneously.
This catches design problems before they cost anything. If the frontend team notices the error shape is inconsistent halfway through mocking, fixing the YAML takes minutes. Fixing a deployed API with live clients takes a deprecation cycle.
What a Well-Designed Spec Looks Like
A single endpoint should communicate: success shape, every error condition with its own schema, and what authentication it requires. The error schemas matter most — they are what clients actually need to handle.
# openapi.yaml (partial)
paths:
/projects/{id}:
get:
operationId: getProject
summary: Get a single project by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Project found
content:
application/json:
schema:
$ref: "#/components/schemas/Project"
"401":
description: Not authenticated
content:
application/json:
schema:
$ref: "#/components/schemas/UnauthorizedError"
"403":
description: Authenticated but not permitted
content:
application/json:
schema:
$ref: "#/components/schemas/ForbiddenError"
"404":
description: Project not found
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundError"
components:
schemas:
Project:
type: object
required: [id, title, status, createdAt]
properties:
id:
type: string
format: uuid
title:
type: string
status:
type: string
enum: [draft, active, archived]
createdAt:
type: string
format: date-time
NotFoundError:
type: object
required: [error, code]
properties:
error:
type: string
example: "Project not found"
code:
type: string
enum: [NOT_FOUND]
UnauthorizedError:
type: object
required: [error, code]
properties:
error:
type: string
code:
type: string
enum: [UNAUTHORIZED]
ForbiddenError:
type: object
required: [error, code]
properties:
error:
type: string
code:
type: string
enum: [FORBIDDEN]
The error schemas use a machine-readable code field alongside the human-readable error string. This lets frontend code switch on body.code without string-matching error messages — the messages can change for copy reasons without breaking client logic.
Type Generation from the Spec
openapi-typescript generates TypeScript types directly from the spec. The generated types become the single source of truth for both the frontend HTTP client and the backend response validators.
npx openapi-typescript ./openapi.yaml -o ./src/types/api.d.ts
The generated file contains types for every request, response, and schema in the spec. Use openapi-fetch to wire them to a typed fetch client:
import createClient from "openapi-fetch";
import type { paths } from "./types/api";
const client = createClient<paths>({ baseUrl: "/api" });
// TypeScript knows the return type, the path params, and the error shapes
const { data, error } = await client.GET("/projects/{id}", {
params: { path: { id: projectId } },
});
if (error) {
// error is typed — code is "NOT_FOUND" | "UNAUTHORIZED" | "FORBIDDEN"
console.error(error.code);
}
The type paths encodes the entire API surface: every path, method, parameter, and response. If the spec changes, regenerate, and TypeScript points to every broken usage.
The Spec as Source of Truth
When the spec is authoritative, the implementation serves the spec — not the other way around. Validation libraries like @readme/openapi-validator or Zod schemas generated from the spec can verify at runtime that responses match what was promised.
This matters most at the boundary between teams. When the frontend knows it can trust the spec, it can write types and client code before the backend is deployed. When the backend knows the spec is the test, it implements against a clear target rather than guessing what the frontend needs.
Writing the spec first is not a ceremony. It is the API design session.