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 1000+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Projects scoped and quoted individually
- 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
Recommended Reading
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.
API engineering services → | Start a project →
External Resources
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 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
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 web platform built?
We build SaaS and business platforms — multi-tenant web applications with subscription billing, authentication, admin panels and robust APIs — from architecture through deployment.