Skip to content
rest-rpc
Esc
navigateopen⌘Jpreview
On this page

OpenAPI

Generate OpenAPI document from the contract.

createOpenApiDocument() generates OpenAPI document object from HTTP route declarations.

import { createOpenApiDocument } from "@rest-rpc/core";

const document = createOpenApiDocument(api, {
	info: {
		title: "Todo API",
		version: "1.0.0",
	},
	servers: [{ url: "https://api.example.com" }],
	schemaConverter,
});

What Is Included

OpenAPI generation includes HTTP routes.

It maps:

  • route methods and paths
  • path params
  • query params
  • request headers
  • request bodies
  • status-keyed responses
  • custom body content types
  • route openApi metadata
  • shared commonOpenApi metadata

WebSocket routes are skipped as there is no faithful mapping to OpenAPI.

Schema Conversion

Standard Schema defines validation behavior, not JSON Schema conversion.

OpenAPI generation needs a project-provided schemaConverter.

import {
	createOpenApiDocument,
	isTypeOnlySchema,
	looseJsonSchema,
} from "@rest-rpc/core";
import { z } from "zod";

const document = createOpenApiDocument(api, {
	info: {
		title: "Todo API",
		version: "1.0.0",
	},
	schemaConverter: (schema, { io }) => {
		if (isTypeOnlySchema(schema)) {
			return looseJsonSchema(schema);
		}

		if (schema["~standard"].vendor === "zod") {
			return z.toJSONSchema(schema as z.ZodType, { io });
		}

		return looseJsonSchema(schema);
	},
});

Use a precise converter when your schema library supports JSON Schema output. Use looseJsonSchema() when a loose OpenAPI shape is acceptable. If you have single validation library, you can just pass the library’s JSON Schema converter directly.

Route Metadata

Use openApi on a route for operation metadata.

create: {
	method: "POST",
	path: "/todos",
	body: {
		title: z.string().min(1),
	},
	response: todoSchema,
	openApi: {
		summary: "Create a todo",
		operationId: "createTodo",
		responses: {
			201: {
				description: "Todo created.",
			},
		},
	},
}

Status-keyed responses produce status-keyed OpenAPI responses.

create: {
	method: "POST",
	path: "/todos",
	body: {
		title: z.string().min(1),
	},
	responses: {
		201: todoSchema,
		409: z.object({
			code: z.literal("TODO_ALREADY_EXISTS"),
		}),
	},
	openApi: {
		summary: "Create a todo",
		operationId: "createTodo",
		responses: {
			201: {
				description: "Todo created.",
			},
			409: {
				description: "A todo with the same title already exists.",
			},
		},
	},
}

openApi.responses only affects generated OpenAPI output. It does not change rest-rpc runtime behavior, handler return types, client response types, validation, or server headers. Metadata for statuses that a route does not declare is skipped.

openApi: {
	responses: {
		200: {
			headers: {
				"x-request-id": {
					description: "Request correlation id.",
					schema: z.string(),
				},
				"x-rate-limit": z.number(),
			},
		},
	},
}

Use route response headers instead when a handler-owned header is part of the typed rest-rpc contract.

Shared Metadata

Use commonOpenApi on router() options to apply metadata across a tree.

export const api = router(routes, {
	commonOpenApi: {
		tags: ["Todos"],
		security: [{ bearerAuth: [] }],
		responses: {
			200: {
				headers: {
					"x-request-id": z.string(),
				},
			},
			401: {
				description: "Authentication is required.",
			},
		},
	},
});

commonOpenApi.responses is merged into each route’s openApi.responses. Route-level OpenAPI metadata overrides common metadata. Typed headers declared on a route response override OpenAPI-only headers with the same name in generated OpenAPI.

Transform Hooks

Use transformOperation and transformDocument when generated output needs project-specific changes.

const document = createOpenApiDocument(api, {
	info: {
		title: "Todo API",
		version: "1.0.0",
	},
	schemaConverter,
	transformOperation: ({ route, operation }) => {
		if (route.metadata.auth === "required") {
			return {
				...operation,
				security: [{ bearerAuth: [] }],
			};
		}

		return operation;
	},
});

Was this page helpful?