API-First Design with OpenAPI 3.1: Schema Design, Code Generation, and Contract Testing
Design APIs first with OpenAPI 3.1 — schema design patterns, TypeScript code generation with openapi-typescript, contract testing with Prism, request validation
API-First Design with OpenAPI 3.1: Schema Design, Code Generation, and Contract Testing
API-first development inverts the typical workflow: instead of building an API and then documenting it, you write the OpenAPI specification first — then generate server types, client types, mock servers, and validation from that single source of truth.
The payoff: your API documentation is always accurate (generated, not written), your TypeScript types are consistent across client and server, and your tests can validate against the contract rather than implementation.
OpenAPI 3.1 Specification Structure
# openapi.yaml
openapi: 3.1.0
info:
title: Acme API
version: 1.0.0
description: |
REST API for Acme SaaS platform.
## Authentication
All endpoints require Bearer token authentication.
Obtain tokens via POST /auth/login.
servers:
- url: https://api.yourapp.com/v1
description: Production
- url: https://api.staging.yourapp.com/v1
description: Staging
- url: http://localhost:3000/v1
description: Local development
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
# Reusable schemas — define once, reference everywhere
UUID:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
Timestamp:
type: string
format: date-time
example: "2026-05-23T10:30:00Z"
Error:
type: object
required: [code, message]
properties:
code:
type: string
example: "VALIDATION_ERROR"
message:
type: string
example: "The request body is invalid"
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
PaginatedMeta:
type: object
required: [total, page, perPage, totalPages]
properties:
total:
type: integer
example: 142
page:
type: integer
example: 1
perPage:
type: integer
example: 20
totalPages:
type: integer
example: 8
Project:
type: object
required: [id, name, status, createdAt, updatedAt]
properties:
id:
$ref: '#/components/schemas/UUID'
name:
type: string
minLength: 1
maxLength: 255
example: "Mobile App Redesign"
description:
type: string
nullable: true
maxLength: 5000
status:
type: string
enum: [active, archived, deleted]
example: "active"
memberCount:
type: integer
minimum: 0
example: 5
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
CreateProjectRequest:
type: object
required: [name]
properties:
name:
type: string
minLength: 1
maxLength: 255
description:
type: string
maxLength: 5000
security:
- BearerAuth: []
paths:
/projects:
get:
operationId: listProjects
summary: List projects
tags: [Projects]
parameters:
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
- name: perPage
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: status
in: query
schema:
type: string
enum: [active, archived]
responses:
'200':
description: Paginated list of projects
content:
application/json:
schema:
type: object
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Project'
meta:
$ref: '#/components/schemas/PaginatedMeta'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
operationId: createProject
summary: Create a project
tags: [Projects]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProjectRequest'
responses:
'201':
description: Project created
content:
application/json:
schema:
$ref: '#/components/schemas/Project'
'400':
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
TypeScript Code Generation
Generate types from your OpenAPI spec — never write API types manually:
# Install openapi-typescript
npm install -D openapi-typescript
# Generate types from spec
npx openapi-typescript openapi.yaml -o src/types/api.d.ts
// src/types/api.d.ts (generated — do not edit manually)
export interface paths {
'/projects': {
get: operations['listProjects'];
post: operations['createProject'];
};
'/projects/{projectId}': {
get: operations['getProject'];
patch: operations['updateProject'];
delete: operations['deleteProject'];
};
}
export interface components {
schemas: {
Project: {
id: string;
name: string;
description?: string | null;
status: 'active' | 'archived' | 'deleted';
memberCount: number;
createdAt: string;
updatedAt: string;
};
CreateProjectRequest: {
name: string;
description?: string;
};
Error: {
code: string;
message: string;
details?: Array<{ field?: string; message?: string }>;
};
};
}
export type operations = {
listProjects: {
parameters: {
query?: {
page?: number;
perPage?: number;
status?: 'active' | 'archived';
};
};
responses: {
200: {
content: {
'application/json': {
data: components['schemas']['Project'][];
meta: { total: number; page: number; perPage: number; totalPages: number };
};
};
};
};
};
};
// Use generated types in your API client
import type { components, operations } from './types/api';
type Project = components['schemas']['Project'];
type CreateProjectRequest = components['schemas']['CreateProjectRequest'];
type ListProjectsParams = operations['listProjects']['parameters']['query'];
// Type-safe API client
export async function listProjects(
params: ListProjectsParams = {}
): Promise<{ data: Project[]; meta: { total: number; page: number; perPage: number; totalPages: number } }> {
const searchParams = new URLSearchParams(
Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])
);
const response = await fetch(`/api/v1/projects?${searchParams}`, {
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!response.ok) throw new ApiError(await response.json());
return response.json();
}
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Request Validation Middleware
Validate all incoming requests against your OpenAPI spec automatically:
// middleware/openapi-validation.ts
import { OpenApiValidator } from 'express-openapi-validator';
// or for Fastify:
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import * as yaml from 'js-yaml';
import { readFileSync } from 'fs';
const spec = yaml.load(readFileSync('./openapi.yaml', 'utf8')) as Record<string, unknown>;
// Fastify: compile validators from OpenAPI schemas
const ajv = new Ajv({ allErrors: true, coerceTypes: true });
addFormats(ajv);
// Compile request body validator for createProject
const validateCreateProject = ajv.compile({
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 255 },
description: { type: 'string', maxLength: 5000 },
},
additionalProperties: false,
});
app.post('/v1/projects', async (request, reply) => {
// Validate request body against OpenAPI schema
if (!validateCreateProject(request.body)) {
return reply.code(400).send({
code: 'VALIDATION_ERROR',
message: 'The request body is invalid',
details: validateCreateProject.errors?.map(e => ({
field: e.instancePath.replace('/', ''),
message: e.message,
})),
});
}
// ... route handler
});
// Better: use fastify-openapi-glue to auto-generate routes from spec
import fastifyOpenapiGlue from 'fastify-openapi-glue';
app.register(fastifyOpenapiGlue, {
specification: './openapi.yaml',
serviceHandlers: './src/handlers',
// Auto-validates all requests and responses against spec
});
Contract Testing with Prism
Prism runs a mock server from your OpenAPI spec — frontend teams can develop before the backend exists:
# Install Prism
npm install -g @stoplight/prism-cli
# Start mock server
prism mock openapi.yaml --port 4010
# All endpoints from your spec are immediately available
curl http://localhost:4010/v1/projects
# Returns: realistic mock data matching your schema
# Validate that your real server conforms to the spec
prism proxy openapi.yaml http://localhost:3000 --port 4020
# Sends requests to real server, validates responses match spec
# Reports any schema violations
// tests/contract.test.ts — verify your server matches the spec
import { createServer } from '@stoplight/prism-http';
describe('API Contract Tests', () => {
let prism: Awaited<ReturnType<typeof createServer>>;
beforeAll(async () => {
prism = await createServer(spec, {
mock: { dynamic: true },
validateRequest: true,
validateResponse: true,
});
});
it('GET /projects returns valid schema', async () => {
const response = await fetch('http://localhost:3000/v1/projects', {
headers: { Authorization: 'Bearer test-token' },
});
// Validate response against OpenAPI schema
const result = await prism.request({
method: 'GET',
url: { path: '/projects', query: {} },
headers: { authorization: 'Bearer test-token' },
}, response);
expect(result.violations).toHaveLength(0);
});
});
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Keeping Spec and Code in Sync
# .github/workflows/api-spec.yml
name: API Spec Validation
on: [pull_request]
jobs:
validate-spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Validate OpenAPI spec is valid
- name: Validate OpenAPI spec
run: npx @redocly/cli lint openapi.yaml
# Check generated types are up to date
- name: Regenerate types
run: npx openapi-typescript openapi.yaml -o src/types/api.gen.d.ts
- name: Check for type drift
run: git diff --exit-code src/types/api.d.ts
# Fails if developer changed the generated file manually
# Run contract tests against the spec
- name: Contract tests
run: |
npx prism mock openapi.yaml --port 4010 &
sleep 2
npm run test:contract
API Versioning Strategy
# Versioning in OpenAPI
servers:
- url: https://api.yourapp.com/v1
description: Current stable API
- url: https://api.yourapp.com/v2
description: Next version (beta)
# Header-based versioning alternative:
# All requests to /api/* with header: API-Version: 2026-05
# Stripe uses this pattern
Working With Viprasol
We design APIs with OpenAPI-first methodology — specification design, TypeScript code generation, validation middleware, mock servers for parallel development, and documentation hosted with Redoc or Swagger UI. Well-designed APIs accelerate frontend development and reduce integration errors.
→ Talk to our team about API design and documentation.
See Also
- API Versioning Strategies — managing API evolution
- GraphQL vs REST vs gRPC — choosing the right API paradigm
- API Documentation Tools — tools for publishing API docs
- TypeScript Advanced Patterns — TypeScript for API type safety
- Web Development Services — API design and development
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.