typescript 150 lines · 4 tabs

OpenAPI generation for REST endpoints

Shared by codesnips Jan 2026
4 tabs
import { OpenAPIRegistry, extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

extendZodWithOpenApi(z);

export const registry = new OpenAPIRegistry();

export interface RouteConfig {
  method: 'get' | 'post' | 'put' | 'patch' | 'delete';
  path: string;
  summary: string;
  body?: z.ZodTypeAny;
  params?: z.AnyZodObject;
  query?: z.AnyZodObject;
  responses: Record<number, { description: string; schema?: z.ZodTypeAny }>;
}

export function defineRoute(config: RouteConfig): RouteConfig {
  registry.registerPath({
    method: config.method,
    path: config.path,
    summary: config.summary,
    request: {
      params: config.params,
      query: config.query,
      body: config.body
        ? { content: { 'application/json': { schema: config.body } } }
        : undefined,
    },
    responses: Object.fromEntries(
      Object.entries(config.responses).map(([status, r]) => [
        status,
        {
          description: r.description,
          content: r.schema
            ? { 'application/json': { schema: r.schema } }
            : undefined,
        },
      ]),
    ),
  });
  return config;
}
4 files · typescript Explain with highlit

This snippet shows how a single Zod schema per route can drive both runtime request validation and a generated OpenAPI 3.1 document, eliminating the drift that plagues hand-written swagger files. The core idea is that the schema is the source of truth: rather than describing an endpoint twice (once for validation, once for docs), each route registers its schemas in one place and both artifacts are derived from that registration.

In registry.ts, @asteasolutions/zod-to-openapi extends Zod with an .openapi() method and provides an OpenAPIRegistry. The defineRoute helper wraps registry.registerPath, so calling it records the method, path, request body, params, and response schemas in a central registry. Crucially it returns the same config object back, which lets the caller reuse those exact schemas for validation — the registration and the validation contract can never diverge because they are literally the same objects.

validate.ts turns a route config into Express middleware. validateRequest parses req.body, req.query, and req.params against the registered schemas using safeParse, and on failure responds with a 400 whose body matches the shape declared for error responses. Using safeParse instead of parse avoids throwing inside the request path and keeps error handling explicit. The parsed, coerced values are written back onto the request so downstream handlers receive typed, validated data rather than raw strings.

users.routes.ts ties it together: it defines CreateUserBody and UserResponse schemas with .openapi() metadata, registers the POST /users route via defineRoute, and mounts the handler behind validateRequest. Because the schema carries example values and descriptions, the generated spec is rich without extra annotation.

generate-spec.ts walks the populated registry with OpenApiGeneratorV31 and writes openapi.json, a step suitable for a build or CI job so the committed spec always reflects the code. The main trade-off is coupling the API to Zod and a specific generator, but in exchange documentation, validation, and TypeScript types stay in lockstep. A common pitfall is forgetting to import route modules before generation — the registry is only populated as a side effect of those imports, so the generator must load every route file first.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab

Share this code

Here's the card — post it anywhere.

OpenAPI generation for REST endpoints — share card
Link copied