@contract-kit/openapi
v1.0.0
Published
OpenAPI 3.1 generation from contract-kit contracts (requires Zod schemas for JSON Schema conversion)
Maintainers
Readme
@contract-kit/openapi
OpenAPI 3.1 generation from Contract Kit contracts
This package generates OpenAPI 3.1 specifications from your contracts. Currently supports Zod schemas for JSON Schema conversion.
Installation
npm install @contract-kit/openapi @contract-kit/core zodTypeScript requirements
This package requires TypeScript 5.0 or higher for proper type inference.
Usage
Generating an OpenAPI spec
import { contractsToOpenAPI } from "@contract-kit/openapi";
import { getTodo, listTodos, createTodo, updateTodo, deleteTodo } from "./contracts/todos";
const spec = contractsToOpenAPI(
[getTodo, listTodos, createTodo, updateTodo, deleteTodo],
{
title: "Todo API",
version: "1.0.0",
description: "A simple todo API built with Contract Kit",
servers: [
{ url: "https://api.example.com", description: "Production" },
{ url: "http://localhost:3000", description: "Development" },
],
}
);
// Write to file
import { writeFileSync } from "fs";
writeFileSync("openapi.json", JSON.stringify(spec, null, 2));Adding OpenAPI metadata to contracts
Use the .openapi() method to add OpenAPI-specific metadata:
import { createContractGroup } from "@contract-kit/core";
import { errors } from "./errors";
import { z } from "zod";
const todos = createContractGroup().namespace("todos");
export const getTodo = todos
.get("/api/todos/:id")
.pathParams(z.object({ id: z.string().describe("Todo ID") }))
.headers(z.object({
authorization: z.string().describe("Bearer token"),
}))
.responses({ 200: TodoSchema })
.errors({ TodoNotFound: errors.TodoNotFound })
.openapi({
summary: "Get a todo by ID",
description: "Retrieves a single todo item by its unique identifier",
tags: ["todos"],
operationId: "getTodoById",
externalDocs: {
url: "https://docs.example.com/todos",
description: "Todo documentation",
},
security: [{ bearerAuth: [] }],
});
export const listTodos = todos
.get("/api/todos")
.query(z.object({
completed: z.boolean().optional().describe("Filter by completion status"),
limit: z.coerce.number().optional().describe("Maximum number of results"),
offset: z.coerce.number().optional().describe("Pagination offset"),
}))
.responses({ 200: z.array(TodoSchema).describe("List of todos") })
.openapi({
summary: "List all todos",
description: "Returns a paginated list of todos with optional filtering",
tags: ["todos"],
operationId: "listTodos",
});
export const createTodo = todos
.post("/api/todos")
.body(CreateTodoSchema.describe("Todo creation payload"))
.responses({ 201: TodoSchema.describe("Created todo") })
.openapi({
summary: "Create a new todo",
tags: ["todos"],
operationId: "createTodo",
});Every path template parameter must have a matching .pathParams(...) field. Missing or extra path parameter keys are rejected to avoid generating invalid OpenAPI paths.
Request headers declared with .headers(...) are emitted as OpenAPI header
parameters. Header keys should be lowercase in contracts; HTTP matching remains
case-insensitive.
Request bodies are supported for POST, PUT, and PATCH contracts only. Contracts that attach a body schema to another method are rejected instead of generating a request body that the client and server will not honor.
Catalog errors declared with .errors(...) use Contract Kit's standard error
envelope. The generator uses catalog messages as response descriptions and adds
named examples with each catalog code and message.
Security schemes
Define authentication schemes in the generator options:
const spec = contractsToOpenAPI(contracts, {
title: "Todo API",
version: "1.0.0",
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
description: "JWT authentication",
},
apiKey: {
type: "apiKey",
name: "X-API-Key",
in: "header",
description: "API key authentication",
},
},
// Global security (applied to all operations by default)
security: [{ bearerAuth: [] }],
});Per-operation security
Override security for specific operations:
// Public endpoint (no auth)
export const healthCheck = api
.get("/api/health")
.responses({ 200: HealthSchema })
.openapi({ security: [{}] }); // Empty object = no security
// Admin-only endpoint
export const deleteUser = api
.delete("/api/users/:id")
.responses({ 204: null })
.openapi({ security: [{ bearerAuth: [], apiKey: [] }] }); // Requires bothTagging operations
Use tags to group operations in the generated documentation:
export const getTodo = todos
.get("/api/todos/:id")
.openapi({ tags: ["todos", "read-operations"] });
export const createTodo = todos
.post("/api/todos")
.openapi({ tags: ["todos", "write-operations"] });OpenAPI method
All OpenAPI metadata is set via the .openapi() method:
| Option | Description |
|--------|-------------|
| summary | Brief summary of the operation |
| description | Detailed description |
| tags | Array of tags for grouping |
| deprecated | Mark operation as deprecated |
| operationId | Custom operation ID (defaults to contract name) |
| externalDocs | { url, description? } - Link to external docs |
| security | Array of security requirements |
Generated schema references
Schemas are automatically extracted and referenced in components/schemas:
{
"openapi": "3.1.0",
"paths": {
"/api/todos/{id}": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/todos_get_response_200" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"todos_get_response_200": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"completed": { "type": "boolean" }
}
}
}
}
}API reference
contractsToOpenAPI(contracts, options)
Converts an array of contracts to an OpenAPI 3.1 document.
function contractsToOpenAPI(
contracts: ContractInput[],
options: OpenAPIGeneratorOptions
): OpenAPIObject;OpenAPIGeneratorOptions
type OpenAPIGeneratorOptions = {
// Required
title: string;
version: string;
// Optional
description?: string;
servers?: OpenAPIServer[];
securitySchemes?: Record<string, OpenAPISecurityScheme>;
security?: Array<Record<string, string[]>>;
jsonMediaType?: string; // default: "application/json"
};OpenAPIServer
type OpenAPIServer = {
url: string;
description?: string;
variables?: Record<string, {
default: string;
enum?: string[];
description?: string;
}>;
};OpenAPISecurityScheme
type OpenAPISecurityScheme =
| { type: "apiKey"; name: string; in: "query" | "header" | "cookie" }
| { type: "http"; scheme: string; bearerFormat?: string }
| { type: "oauth2"; flows: Record<string, unknown> }
| { type: "openIdConnect"; openIdConnectUrl: string };Serving the spec
With Next.js
// app/api/openapi/route.ts
import { contractsToOpenAPI } from "@contract-kit/openapi";
import { allContracts } from "@/contracts";
const spec = contractsToOpenAPI(allContracts, {
title: "My API",
version: "1.0.0",
});
export function GET() {
return Response.json(spec);
}With Swagger UI
// app/api/docs/route.ts
export function GET() {
const html = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({
url: "/api/openapi",
dom_id: "#swagger-ui",
});
</script>
</body>
</html>
`;
return new Response(html, {
headers: { "Content-Type": "text/html" },
});
}Limitations
- Currently requires Zod v4 schemas and uses Zod's built-in JSON Schema conversion
- Core contracts, the server, and the client can still use any Standard Schema-compatible library
Related packages
@contract-kit/core- Core contract definitions@contract-kit/client- HTTP client@contract-kit/next- Next.js adapter@contract-kit/server- Server runtime
License
MIT
