Back to Blog

API Documentation with OpenAPI 3.1: TypeSpec, Spec Generation

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.

Viprasol Tech Team
13 min read
Updated 2026

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

> **Quick answer.** Avoid both documentation drift and unreadable generated docs by generating OpenAPI 3.1 specs from your TypeScript types and validation schemas, so they stay accurate, while writing human descriptions and examples separately. CI fails when the spec drifts. OpenAPI 3.1 aligns with JSON Schema draft 2020-12 for richer schema definitions than 3.0.
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
  • 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" },
  });
}

api - API Documentation with OpenAPI 3.1: TypeSpec, Spec Generation

๐Ÿš€ 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

More on This Topic


Viprasol in Action

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 โ†’

apiopenapitypescriptdocumentationdevops
Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol ยท Web Development

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.