API Documentation with OpenAPI 3.1: TypeSpec, Spec Generation, and Docs-as-Code
Build production API documentation with OpenAPI 3.1: generate specs from TypeScript code with Zod, author with Microsoft TypeSpec, serve with Scalar or Swagger UI, and integrate docs into your CI/CD pipeline as a first-class artifact.
API documentation has two failure modes: documentation that's maintained separately from code and drifts out of sync, and documentation generated from code that's technically accurate but useless to read.
The 2026 approach that avoids both: generate OpenAPI specs from your TypeScript types and validation schemas (so they're always accurate), then write human-readable descriptions and examples separately (so they're actually useful). CI fails if the spec drifts.
OpenAPI 3.1 Basics
OpenAPI 3.1 aligns with JSON Schema draft 2020-12, enabling richer schema definitions than 3.0:
# openapi/base.yaml โ start here
openapi: 3.1.0
info:
title: Viprasol API
version: 2.0.0
description: |
REST API for the Viprasol platform.
## Authentication
All endpoints require a Bearer token obtained from `/auth/token`.
## Rate Limiting
- 1,000 requests/minute per token (standard tier)
- 10,000 requests/minute (enterprise tier)
Rate limit headers are included in every response:
`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
contact:
name: Viprasol Engineering
url: https://viprasol.com/contact
email: api@viprasol.com
servers:
- url: https://api.viprasol.com/v2
description: Production
- url: https://api.staging.viprasol.com/v2
description: Staging
- url: http://localhost:3000/api/v2
description: Local development
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: [] # Apply to all endpoints by default
Generating OpenAPI from Zod Schemas
zod-to-openapi generates an OpenAPI spec from your existing Zod schemas โ no duplication:
// src/api/openapi/registry.ts
import {
OpenAPIRegistry,
OpenApiGeneratorV31,
extendZodWithOpenApi,
} from "@asteasolutions/zod-to-openapi";
import { z } from "zod";
// Extend Zod with OpenAPI metadata methods
extendZodWithOpenApi(z);
export const registry = new OpenAPIRegistry();
// Define reusable schemas
const UserSchema = registry.register(
"User",
z.object({
id: z.string().uuid().openapi({ description: "User's unique identifier" }),
email: z.string().email().openapi({ example: "user@example.com" }),
name: z.string().min(1).max(100).openapi({ example: "Jane Smith" }),
plan: z
.enum(["free", "starter", "growth", "enterprise"])
.openapi({ description: "Subscription plan" }),
createdAt: z.string().datetime().openapi({
description: "ISO 8601 timestamp",
example: "2026-01-15T10:30:00Z",
}),
})
);
const PaginationSchema = z.object({
page: z
.number()
.int()
.min(1)
.default(1)
.openapi({ description: "Page number (1-indexed)" }),
perPage: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.openapi({ description: "Results per page" }),
});
const ErrorSchema = registry.register(
"ApiError",
z.object({
error: z.string().openapi({ example: "RESOURCE_NOT_FOUND" }),
message: z.string().openapi({ example: "User not found" }),
details: z.record(z.string(), z.array(z.string())).optional().openapi({
description: "Field-level validation errors",
example: { email: ["Invalid email format"] },
}),
})
);
// Register endpoints
registry.registerPath({
method: "get",
path: "/users",
summary: "List users",
description:
"Returns a paginated list of users for the authenticated tenant. " +
"Results are sorted by `createdAt` descending.",
tags: ["Users"],
request: {
query: PaginationSchema.extend({
search: z.string().optional().openapi({
description: "Filter by name or email (partial match)",
}),
plan: z
.enum(["free", "starter", "growth", "enterprise"])
.optional()
.openapi({ description: "Filter by subscription plan" }),
}),
},
responses: {
200: {
description: "Paginated list of users",
content: {
"application/json": {
schema: z.object({
data: z.array(UserSchema),
pagination: z.object({
page: z.number().int(),
perPage: z.number().int(),
total: z.number().int(),
totalPages: z.number().int(),
}),
}),
},
},
},
401: {
description: "Missing or invalid authentication token",
content: { "application/json": { schema: ErrorSchema } },
},
},
});
registry.registerPath({
method: "post",
path: "/users",
summary: "Create a user",
description: "Creates a new user in the authenticated tenant. Sends a welcome email.",
tags: ["Users"],
request: {
body: {
description: "User creation payload",
required: true,
content: {
"application/json": {
schema: z.object({
email: z.string().email().openapi({ example: "new@example.com" }),
name: z.string().min(1).max(100).openapi({ example: "Alex Johnson" }),
role: z
.enum(["member", "admin"])
.default("member")
.openapi({ description: "Role within the tenant" }),
}),
},
},
},
},
responses: {
201: {
description: "User created successfully",
content: { "application/json": { schema: UserSchema } },
},
409: {
description: "A user with this email already exists",
content: { "application/json": { schema: ErrorSchema } },
},
422: {
description: "Validation failed",
content: { "application/json": { schema: ErrorSchema } },
},
},
});
Generate and Write the Spec
// src/scripts/generate-openapi.ts
import { OpenApiGeneratorV31 } from "@asteasolutions/zod-to-openapi";
import { registry } from "../api/openapi/registry";
import { writeFileSync } from "fs";
import yaml from "js-yaml";
const generator = new OpenApiGeneratorV31(registry.definitions);
const spec = generator.generateDocument({
openapi: "3.1.0",
info: {
title: "Viprasol API",
version: "2.0.0",
description: "Viprasol Platform REST API",
},
servers: [
{ url: "https://api.viprasol.com/v2", description: "Production" },
],
});
// Write both JSON and YAML formats
writeFileSync("openapi/spec.json", JSON.stringify(spec, null, 2));
writeFileSync("openapi/spec.yaml", yaml.dump(spec, { lineWidth: 120 }));
console.log("OpenAPI spec generated successfully.");
// package.json scripts
{
"scripts": {
"openapi:generate": "tsx src/scripts/generate-openapi.ts",
"openapi:validate": "openapi-cli validate openapi/spec.yaml",
"openapi:lint": "redocly lint openapi/spec.yaml"
}
}
๐ 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
Microsoft TypeSpec (Design-First Approach)
TypeSpec is a meta-language for defining APIs. Write TypeSpec, generate OpenAPI (and other outputs):
// api/main.tsp โ TypeSpec definition
import "@typespec/http";
import "@typespec/rest";
import "@typespec/openapi3";
using TypeSpec.Http;
using TypeSpec.Rest;
@service({ title: "Viprasol API", version: "2.0.0" })
@server("https://api.viprasol.com/v2", "Production")
namespace ViprasolApi;
// Shared models
model User {
id: string;
email: string;
name: string;
plan: "free" | "starter" | "growth" | "enterprise";
createdAt: utcDateTime;
}
model PaginatedUsers {
data: User[];
pagination: {
page: int32;
perPage: int32;
total: int32;
totalPages: int32;
};
}
model ApiError {
error: string;
message: string;
details?: Record<string[]>;
}
// Users resource
@route("/users")
@tag("Users")
interface Users {
@doc("List users for the authenticated tenant")
@get
list(
@query page?: int32 = 1,
@query perPage?: int32 = 20,
@query search?: string,
@query plan?: "free" | "starter" | "growth" | "enterprise"
): PaginatedUsers | ApiError;
@doc("Create a new user")
@post
create(
@body body: {
email: string;
name: string;
role?: "member" | "admin";
}
): {
@statusCode statusCode: 201;
@body user: User;
} | {
@statusCode statusCode: 409;
@body error: ApiError;
};
@doc("Get a specific user")
@get
@route("/{id}")
read(@path id: string): User | ApiError;
}
# Generate OpenAPI from TypeSpec
npx tsp compile api/main.tsp --emit @typespec/openapi3
# Output: tsp-output/@typespec/openapi3/openapi.yaml
Serving Documentation
Scalar (Recommended over Swagger UI)
// src/app/api/docs/route.ts
// Serve API docs at /api/docs
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const spec = await import("../../../openapi/spec.json");
// Scalar: modern, clean UI with better DX than Swagger UI
const html = `<!DOCTYPE html>
<html>
<head>
<title>Viprasol API Documentation</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script
id="api-reference"
type="application/json"
>${JSON.stringify(spec)}</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>`;
return new NextResponse(html, {
headers: { "Content-Type": "text/html" },
});
}
๐ 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
CI/CD Integration (Docs-as-Code)
# .github/workflows/openapi.yml
name: API Documentation
on:
push:
branches: [main]
pull_request:
paths:
- "src/api/**"
- "openapi/**"
jobs:
validate-spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
run: npm ci
- name: Generate OpenAPI spec
run: npm run openapi:generate
- name: Check for spec drift
# Fail if generated spec differs from committed spec
run: |
if ! git diff --exit-code openapi/spec.yaml; then
echo "::error::OpenAPI spec is out of date. Run 'npm run openapi:generate' and commit the result."
exit 1
fi
- name: Validate spec
run: npm run openapi:validate
- name: Lint spec
run: npx @redocly/cli lint openapi/spec.yaml
- name: Check for breaking changes (on PRs)
if: github.event_name == 'pull_request'
run: |
git fetch origin main
npx @optic/cli diff \
openapi/spec.yaml \
--base $(git show origin/main:openapi/spec.yaml) \
--check # Fails on breaking changes
- name: Publish to API portal (main branch only)
if: github.ref == 'refs/heads/main'
run: |
npx @redocly/cli push \
openapi/spec.yaml \
@viprasol/production@latest \
--organization viprasol
env:
REDOCLY_AUTHORIZATION: ${{ secrets.REDOCLY_API_KEY }}
Writing Useful Descriptions
Generated schemas are accurate but need human-written context to be useful:
# Good OpenAPI description
/payments:
post:
summary: Create a payment
description: |
Creates a payment charge against the provided payment method.
**Idempotency**: Pass an `Idempotency-Key` header to safely retry requests
without creating duplicate charges. The key must be unique per charge attempt.
**Webhooks**: A `payment.succeeded` or `payment.failed` event is sent to your
webhook endpoint after the charge completes (typically within 5 seconds).
**Test mode**: Use card number `4242 4242 4242 4242` with any future expiry
to simulate successful charges in the staging environment.
# Bad description (avoid):
# description: Creates a payment. โ Restates the summary, adds no value
See Also
- API Versioning Strategies โ versioning patterns
- API Security Best Practices โ auth, rate limiting
- API Rate Limiting: Advanced Patterns โ implementation
- API Gateway Patterns โ routing, aggregation
Working With Viprasol
Good API documentation is an engineering deliverable, not a task for the end of the sprint. We build API documentation into the development workflow โ generating accurate specs from TypeScript types, writing human-readable descriptions that developers can actually use, and validating spec consistency in CI so documentation is always current.
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.