Overview
@contract-kit/openapi generates OpenAPI 3.1 specifications from your Contract Kit contracts. It automatically converts your contracts and schemas into comprehensive API documentation that works with tools like Swagger UI, Redoc, and Postman.
Prefer the contract-kit meta package for new projects. It re-exports
@contract-kit/openapi along with core, client, application, and more.
Installation
npm install @contract-kit/openapi @contract-kit/core zod
Quick Start
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 ));
Use the fluent builder methods to add OpenAPI-specific metadata to your contracts:
import { createContractGroup } from " @contract-kit/core " ;
import { z } from " zod " ;
const todos = createContractGroup (). namespace ( " todos " );
export const getTodo = todos
. get ( " /api/todos/:id " )
. path (z. object ({ id : z. string (). describe ( " Todo ID " ) }))
. response ( 200 , TodoSchema)
. errors ({
404 : z. object ({
code : z. literal ( " TODO_NOT_FOUND " ),
message : z. string (),
}). describe ( " Todo not found " ),
})
// OpenAPI metadata
. summary ( " Get a todo by ID " )
. description ( " Retrieves a single todo item by its unique identifier " )
. tags ( " todos " )
. operationId ( " getTodoById " )
. externalDocs ( " https://docs.example.com/todos " , " Todo documentation " )
. security ({ bearerAuth : [] });
Schema Descriptions
Add descriptions to schemas for better documentation:
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 " ),
}))
. response ( 200 , z. array (TodoSchema). describe ( " List of todos " ))
. summary ( " List all todos " )
. description ( " Returns a paginated list of todos with optional filtering " )
. tags ( " todos " )
. operationId ( " listTodos " );
OpenAPI Builder Methods
Method Description .summary(text)Brief summary of the operation .description(text)Detailed description .tags(...tags)Tags for grouping (accumulates) .deprecated(flag)Mark operation as deprecated .operationId(id)Custom operation ID (defaults to contract name) .externalDocs(url, description?)Link to external documentation .security(requirement)Security requirements (accumulates)
Security Schemes
Defining 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 " )
. response ( 200 , HealthSchema)
. security ({}); // Empty object = no security
// Admin-only endpoint
export const deleteUser = api
. delete ( " /api/users/:id " )
. response ( 204 , z. null ())
. security ({ bearerAuth : [], apiKey : [] }); // Requires both
Tagging Operations
Use tags to group operations in the generated documentation:
// Add tags to individual operations
export const getTodo = todos
. get ( " /api/todos/:id " )
. tags ( " todos " , " read-operations " );
// Tags accumulate across the chain
export const createTodo = todos
. post ( " /api/todos " )
. tags ( " todos " )
. tags ( " write-operations " ); // Now has both tags
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 " },
});
}
With Redoc
// app/api/docs/route.ts
export function GET () {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>API Documentation</title>
</head>
<body>
<redoc spec-url="/api/openapi"></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
</body>
</html>
` ;
return new Response (html, {
headers : { " Content-Type " : " text/html " },
});
}
Complete Example
// contracts/todos.ts
import { createContractGroup } from " @contract-kit/core " ;
import { z } from " zod " ;
const todos = createContractGroup ()
. namespace ( " todos " )
. errors ({
401 : z. object ({ message : z. string () }),
500 : z. object ({ message : z. string () }),
});
const TodoSchema = z. object ({
id : z. string (). describe ( " Unique identifier " ),
title : z. string (). describe ( " Todo title " ),
completed : z. boolean (). describe ( " Completion status " ),
createdAt : z. string (). datetime (). describe ( " Creation timestamp " ),
});
export const listTodos = todos
. get ( " /api/todos " )
. query (z. object ({
completed : z. boolean (). optional (). describe ( " Filter by completion status " ),
limit : z.coerce. number (). min ( 1 ). max ( 100 ). optional (). describe ( " Page size " ),
offset : z.coerce. number (). min ( 0 ). optional (). describe ( " Pagination offset " ),
}))
. response ( 200 , z. object ({
todos : z. array (TodoSchema),
total : z. number (),
}))
. summary ( " List todos " )
. description ( " Returns a paginated list of todos with optional filtering by completion status " )
. tags ( " todos " )
. operationId ( " listTodos " );
export const getTodo = todos
. get ( " /api/todos/:id " )
. path (z. object ({ id : z. string (). describe ( " Todo ID " ) }))
. response ( 200 , TodoSchema)
. errors ({
404 : z. object ({ message : z. string () }),
})
. summary ( " Get a todo " )
. description ( " Retrieves a single todo by its ID " )
. tags ( " todos " )
. operationId ( " getTodo " );
export const createTodo = todos
. post ( " /api/todos " )
. body (z. object ({
title : z. string (). min ( 1 ). max ( 100 ). describe ( " Todo title " ),
completed : z. boolean (). optional (). describe ( " Initial completion status " ),
}))
. response ( 201 , TodoSchema)
. summary ( " Create a todo " )
. description ( " Creates a new todo item " )
. tags ( " todos " )
. operationId ( " createTodo " );
// app/api/openapi/route.ts
import { contractsToOpenAPI } from " @contract-kit/openapi " ;
import * as todoContracts from " @/contracts/todos " ;
const spec = contractsToOpenAPI (
[
todoContracts.listTodos,
todoContracts.getTodo,
todoContracts.createTodo,
],
{
title : " Todo API " ,
version : " 1.0.0 " ,
description : " A simple todo management API " ,
servers : [
{ url : " https://api.example.com " , description : " Production " },
{ url : " http://localhost:3000 " , description : " Development " },
],
securitySchemes : {
bearerAuth : {
type : " http " ,
scheme : " bearer " ,
bearerFormat : " JWT " ,
},
},
security : [{ bearerAuth : [] }],
}
);
export function GET () {
return Response. json (spec);
}
Limitations
Currently requires Zod schemas (uses zod-to-json-schema for conversion)
Other Standard Schema libraries will be supported in future versions
Best Practices
Add descriptions everywhere
Use .describe() on all schema fields and add summaries/descriptions to all operations for comprehensive documentation.
Use meaningful operation IDs
Set custom operation IDs with .operationId() for better code generation in client SDKs.
Group operations with tags
Use tags to organize your API endpoints into logical groups in the documentation.
Define error schemas in .errors() to document all possible error responses.
Next Steps