Web Development · API Design
OpenAPI-First API Development: Write the Contract Before the Code
OpenAPI-first API development means writing the spec before code, then generating server types, request validation, and client SDKs from it.
Anurag Verma
7 min read
Sponsored
Most teams write APIs the same way: build the endpoints, wire up the handlers, then generate docs from the running server. OpenAPI comes last, as a description of what was already built. The problem with this order is that the spec becomes a byproduct: accurate when it’s first generated, gradually wrong as the API evolves, rarely updated after the initial release.
OpenAPI-first reverses that. You write the spec first. The spec is the source of truth. Code is generated from the spec, validated against the spec, and clients are distributed as generated SDKs from the spec. Documentation is always correct because it’s the input, not the output.
This isn’t a new idea, but the tooling has matured enough in 2026 that it’s practical for projects of any size. It’s also one option among several for structuring an API in the first place: see our comparison of REST, GraphQL, and tRPC if you’re still deciding on the underlying paradigm before you commit to a spec format.
What You Get From a Good Spec
An OpenAPI 3.1 spec is a YAML or JSON document that describes every endpoint: its path, method, parameters, request body, possible responses, and the schemas for each. When it’s your source of truth, several things follow automatically:
Server-side validation: Requests that don’t match the spec can be rejected before they reach your handler. No manual if (!req.body.email) guards.
Generated client SDKs: Front-end teams get a typed client that matches the API exactly. No hunting through documentation to find the right parameter name.
Contract testing: You can run a test suite that verifies the live server matches the spec. Catch regressions before they reach production. Tools like Pact take this further for microservices — see our contract testing guide for how consumer-driven contracts work across service boundaries.
Consistent error responses: Define your error schemas once; every endpoint uses them.
The Spec
OpenAPI 3.1 adopted JSON Schema fully, which means you can use $ref to share schemas across your spec and use the full JSON Schema vocabulary.
A minimal spec for a user management API:
# openapi.yaml
openapi: 3.1.0
info:
title: User API
version: 1.0.0
paths:
/users:
post:
operationId: createUser
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/ValidationError'
'409':
$ref: '#/components/responses/ConflictError'
/users/{id}:
get:
operationId: getUser
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFoundError'
components:
schemas:
CreateUserRequest:
type: object
required: [email, name]
properties:
email:
type: string
format: email
name:
type: string
minLength: 1
maxLength: 100
User:
type: object
required: [id, email, name, createdAt]
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
createdAt:
type: string
format: date-time
responses:
ValidationError:
description: Request validation failed
content:
application/json:
schema:
type: object
required: [error, details]
properties:
error:
type: string
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
Write this spec before a line of server code exists. Review it with the team (or client). Once everyone agrees on the contract, generate.
Generating Server Types in TypeScript
openapi-typescript generates TypeScript types from a spec with zero runtime overhead:
npm install -D openapi-typescript
npx openapi-typescript openapi.yaml -o src/generated/api.d.ts
The output is a types-only file. You get the schema types and response types for every endpoint:
import type { components, paths } from './generated/api.d.ts';
type User = components['schemas']['User'];
type CreateUserRequest = components['schemas']['CreateUserRequest'];
// The path types tell you exactly what parameters and responses look like
type GetUserResponse = paths['/users/{id}']['get']['responses']['200']['content']['application/json'];
Run this generation step in CI. If the spec changes and the types change, TypeScript will tell you everywhere in the codebase that’s now broken.
Runtime Validation with express-openapi-validator
Types catch errors at compile time. Request validation at runtime catches what clients send that doesn’t match your spec. If you’re validating request bodies by hand instead of from a spec, our Zod schema validation guide covers a common alternative approach.
npm install express-openapi-validator
import express from 'express';
import OpenApiValidator from 'express-openapi-validator';
const app = express();
app.use(express.json());
app.use(
OpenApiValidator.middleware({
apiSpec: './openapi.yaml',
validateRequests: true,
validateResponses: true, // catches response bugs during development
})
);
// Your handlers — request bodies are already validated before reaching here
app.post('/users', async (req, res) => {
const body: CreateUserRequest = req.body; // guaranteed to match the schema
const user = await userService.create(body);
res.status(201).json(user);
});
// Error handler for validation failures
app.use((err: any, req: any, res: any, next: any) => {
if (err.status === 400) {
res.status(400).json({
error: 'Validation failed',
details: err.errors,
});
} else {
next(err);
}
});
With validateResponses: true in development, you’ll get errors when a handler returns a response that doesn’t match the spec. Useful for catching bugs before they reach clients.
Generating Client SDKs
openapi-fetch (from the openapi-ts project) gives you a type-safe fetch client generated directly from your spec:
npm install openapi-fetch
import createClient from 'openapi-fetch';
import type { paths } from './generated/api.d.ts';
const client = createClient<paths>({ baseUrl: 'https://api.yourapp.com' });
// Fully typed — TypeScript knows the params, body, and response type
const { data, error } = await client.POST('/users', {
body: {
email: 'user@example.com',
name: 'Test User',
},
});
if (data) {
console.log(data.id); // string — TypeScript knows this
}
If the spec changes (say you add a required field to CreateUserRequest), the generated types update and every client call that’s missing the field becomes a TypeScript error. The contract is enforced across the codebase automatically.
TypeSpec: Generating OpenAPI From Code
For teams that find writing YAML by hand tedious, TypeSpec (from Microsoft) is the reverse approach: write the API description in a TypeScript-like DSL, generate the OpenAPI spec from it.
npm install -g @typespec/compiler
tsp init
// main.tsp
import "@typespec/http";
using TypeSpec.Http;
@service({ title: "User API" })
namespace UserAPI;
model User {
id: string;
email: string;
name: string;
createdAt: utcDateTime;
}
model CreateUserRequest {
email: string;
name: string;
}
@route("/users")
interface Users {
@post
create(@body body: CreateUserRequest): User | ValidationError;
@get
@route("{id}")
get(@path id: string): User | NotFoundError;
}
tsp compile . --emit @typespec/openapi3
This generates openapi.yaml from the TypeSpec. TypeSpec handles versioning, pagination, and common patterns better than hand-written YAML for large APIs — see our API versioning strategies guide for the tradeoffs between the common versioning approaches once your spec is stable enough to need one.
What This Changes in Practice
The discipline shift is real: you have to agree on the API contract before writing code. That’s a conversation that’s often deferred when teams code-first, which is part of why API design decisions get made late: in code review, or when a client tries to integrate and finds the shape unexpected.
OpenAPI-first makes that conversation happen at the right time. The spec is a document everyone can read, not just developers. Product managers and clients can review the API design before any code exists. Changes to the contract are explicit (they require spec changes) rather than accidental (someone refactors a handler and the response shape quietly changes).
For agencies shipping APIs that clients will integrate against, this matters a lot. A spec-first contract is also a scope boundary: deviations from the agreed spec are change requests, not bugs.
The toolchain (openapi-typescript for types, express-openapi-validator for runtime validation, openapi-fetch for clients) has zero lock-in. The OpenAPI spec is a standard. Switch frameworks, switch languages, switch tools: the spec stays.
Frequently asked questions
- What does OpenAPI-first mean?
- OpenAPI-first means writing the OpenAPI spec before writing any server code, then generating server types, request validation, and client SDKs from that spec. It reverses the more common code-first approach, where the spec is generated afterward from an already-built API and tends to drift out of date.
- What do you get from a good OpenAPI spec?
- A complete spec enables server-side request validation before requests reach your handlers, generated typed client SDKs for front-end teams, contract tests that verify a live server still matches the spec, and consistent error response schemas defined once and reused everywhere.
- How do you generate TypeScript types from an OpenAPI spec?
- The `openapi-typescript` package generates a types-only file from a spec with zero runtime overhead, giving you schema and response types for every endpoint. Running this generation step in CI means any spec change that breaks the codebase shows up as a TypeScript error immediately.
- How is runtime validation different from compile-time type checking for APIs?
- TypeScript types catch mismatches at compile time, but they can't validate what a real client actually sends at runtime. A library like `express-openapi-validator` checks incoming requests against the spec as middleware, rejecting anything that doesn't match before it reaches your handler.
- What is TypeSpec and how does it relate to OpenAPI?
- TypeSpec, built by Microsoft, is the reverse of hand-writing OpenAPI YAML: you describe your API in a TypeScript-like DSL and compile it into an OpenAPI spec. It's aimed at teams that find raw YAML tedious, and it handles versioning and pagination patterns better for large APIs.
Sponsored
More from this category
More from Web Development
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored