
Type Safe Api
- 33 installs
- 27 repo stars
- Updated July 17, 2026
- claude-dev-suite/claude-dev-suite
Ensures end-to-end type safety between backend and frontend using Zod-to-OpenAPI, ts-rest, Zodios, and contract testing.
About
Reference for end-to-end type-safe API patterns covering Zod-to-OpenAPI, ts-rest, Zodios, and contract testing. A developer uses it to share types across TypeScript backend and frontend.
- Zod-to-OpenAPI schema generation
- ts-rest/Zodios contracts and Pact contract testing
Type Safe Api by the numbers
- 33 all-time installs (skills.sh)
- Ranked #3,349 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill type-safe-apiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 27 |
| Last updated | July 17, 2026 |
| Repository | claude-dev-suite/claude-dev-suite ↗ |
What it does
Ensures end-to-end type safety between backend and frontend using Zod-to-OpenAPI, ts-rest, Zodios, and contract testing.
Files
Type-Safe API Core Knowledge
Deep Knowledge: Usemcp__documentation__fetch_docswith technology:type-safe-apifor comprehensive documentation.
Zod to OpenAPI
Generate OpenAPI specs from Zod schemas for type-first development.
npm install @asteasolutions/zod-to-openapi zodDefine Schemas
import { z } from 'zod';
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
extendZodWithOpenApi(z);
// Schema with OpenAPI metadata
export const UserSchema = z.object({
id: z.string().openapi({ example: 'user_123' }),
name: z.string().min(1).openapi({ example: 'John Doe' }),
email: z.string().email().openapi({ example: 'john@example.com' }),
role: z.enum(['user', 'admin']).openapi({ example: 'user' }),
createdAt: z.date().openapi({ example: '2024-01-01T00:00:00Z' }),
}).openapi('User');
export const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true })
.openapi('CreateUser');
export type User = z.infer<typeof UserSchema>;
export type CreateUser = z.infer<typeof CreateUserSchema>;Generate OpenAPI Document
import { OpenAPIRegistry, OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';
const registry = new OpenAPIRegistry();
// Register schemas
registry.register('User', UserSchema);
registry.register('CreateUser', CreateUserSchema);
// Register endpoints
registry.registerPath({
method: 'get',
path: '/users/{id}',
summary: 'Get user by ID',
request: {
params: z.object({ id: z.string() }),
},
responses: {
200: {
description: 'User found',
content: {
'application/json': { schema: UserSchema },
},
},
404: {
description: 'User not found',
},
},
});
registry.registerPath({
method: 'post',
path: '/users',
summary: 'Create user',
request: {
body: {
content: {
'application/json': { schema: CreateUserSchema },
},
},
},
responses: {
201: {
description: 'User created',
content: {
'application/json': { schema: UserSchema },
},
},
},
});
// Generate OpenAPI document
const generator = new OpenApiGeneratorV3(registry.definitions);
const openApiDocument = generator.generateDocument({
openapi: '3.0.0',
info: {
title: 'User API',
version: '1.0.0',
},
servers: [{ url: 'https://api.example.com' }],
});---
ts-rest (Contract-First)
Type-safe REST API contracts shared between client and server.
npm install @ts-rest/core
npm install @ts-rest/next # For Next.js
npm install @ts-rest/react-query # For React QueryDefine Contract
// contracts/api.ts
import { initContract } from '@ts-rest/core';
import { z } from 'zod';
const c = initContract();
export const userContract = c.router({
getUser: {
method: 'GET',
path: '/users/:id',
pathParams: z.object({ id: z.string() }),
responses: {
200: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
404: z.object({ message: z.string() }),
},
},
createUser: {
method: 'POST',
path: '/users',
body: z.object({
name: z.string(),
email: z.string().email(),
}),
responses: {
201: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
400: z.object({ message: z.string() }),
},
},
listUsers: {
method: 'GET',
path: '/users',
query: z.object({
page: z.number().optional(),
limit: z.number().optional(),
}),
responses: {
200: z.array(z.object({
id: z.string(),
name: z.string(),
email: z.string(),
})),
},
},
});Server Implementation (Next.js)
// pages/api/[...ts-rest].ts
import { createNextRoute, createNextRouter } from '@ts-rest/next';
import { userContract } from '../../contracts/api';
const router = createNextRouter(userContract, {
getUser: async ({ params }) => {
const user = await db.user.findUnique({ where: { id: params.id } });
if (!user) {
return { status: 404, body: { message: 'Not found' } };
}
return { status: 200, body: user };
},
createUser: async ({ body }) => {
const user = await db.user.create({ data: body });
return { status: 201, body: user };
},
listUsers: async ({ query }) => {
const users = await db.user.findMany({
skip: ((query.page ?? 1) - 1) * (query.limit ?? 10),
take: query.limit ?? 10,
});
return { status: 200, body: users };
},
});
export default createNextRoute(userContract, router);Client Usage
// lib/api-client.ts
import { initClient } from '@ts-rest/core';
import { userContract } from '../contracts/api';
export const apiClient = initClient(userContract, {
baseUrl: 'https://api.example.com',
baseHeaders: {
Authorization: `Bearer ${getToken()}`,
},
});
// Usage (fully typed)
const { body: user, status } = await apiClient.getUser({ params: { id: '123' } });
const { body: newUser } = await apiClient.createUser({
body: { name: 'John', email: 'john@example.com' },
});React Query Integration
import { initQueryClient } from '@ts-rest/react-query';
import { userContract } from '../contracts/api';
const client = initQueryClient(userContract, {
baseUrl: 'https://api.example.com',
});
// In component
function UserProfile({ id }: { id: string }) {
const { data, isLoading } = client.getUser.useQuery(
['user', id],
{ params: { id } }
);
if (isLoading) return <Spinner />;
return <div>{data?.body.name}</div>;
}---
Zodios (Type-Safe REST Client)
npm install @zodios/core zod
npm install @zodios/react # For React hooksDefine API
import { makeApi, Zodios } from '@zodios/core';
import { z } from 'zod';
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const api = makeApi([
{
method: 'get',
path: '/users/:id',
alias: 'getUser',
response: userSchema,
parameters: [
{ type: 'Path', name: 'id', schema: z.string() },
],
},
{
method: 'post',
path: '/users',
alias: 'createUser',
response: userSchema,
parameters: [
{
type: 'Body',
name: 'body',
schema: z.object({
name: z.string(),
email: z.string().email(),
}),
},
],
},
{
method: 'get',
path: '/users',
alias: 'listUsers',
response: z.array(userSchema),
parameters: [
{ type: 'Query', name: 'status', schema: z.string().optional() },
],
},
]);
export const apiClient = new Zodios('https://api.example.com', api);Client Usage
// Fully typed
const user = await apiClient.getUser({ params: { id: '123' } });
const users = await apiClient.listUsers({ queries: { status: 'active' } });
const newUser = await apiClient.createUser({
name: 'John',
email: 'john@example.com',
});---
Contract Testing
With Pact
npm install -D @pact-foundation/pactimport { Pact } from '@pact-foundation/pact';
const provider = new Pact({
consumer: 'Frontend',
provider: 'UserAPI',
});
describe('User API Contract', () => {
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
afterEach(() => provider.verify());
it('should get user by id', async () => {
await provider.addInteraction({
state: 'user with id 123 exists',
uponReceiving: 'a request to get user 123',
withRequest: {
method: 'GET',
path: '/users/123',
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: '123',
name: 'John Doe',
email: 'john@example.com',
},
},
});
const user = await apiClient.getUser({ params: { id: '123' } });
expect(user.name).toBe('John Doe');
});
});---
Production Readiness
Shared Types Strategy (Monorepo)
packages/
├── api-contracts/ # Shared contracts
│ ├── src/
│ │ ├── schemas.ts # Zod schemas
│ │ ├── types.ts # TypeScript types
│ │ └── contract.ts # ts-rest contract
│ └── package.json
├── backend/
│ ├── src/
│ │ └── routes/ # Implements contracts
│ └── package.json
└── frontend/
├── src/
│ └── api/ # Uses contracts
└── package.jsonBreaking Change Detection
// scripts/check-breaking-changes.ts
import { diff } from 'json-diff';
import oldSpec from './openapi-old.json';
import newSpec from './openapi-new.json';
const changes = diff(oldSpec, newSpec);
const breaking = findBreakingChanges(changes);
if (breaking.length > 0) {
console.error('Breaking changes detected:');
breaking.forEach(console.error);
process.exit(1);
}Checklist
- [ ] Shared schema package in monorepo
- [ ] OpenAPI spec generated from schemas
- [ ] Contract tests between services
- [ ] Breaking change detection in CI
- [ ] Type generation automated
- [ ] Runtime validation on boundaries
- [ ] Error types included in contracts
- [ ] Versioning strategy defined
When NOT to Use This Skill
- tRPC projects (use
trpcskill - simpler for full-stack TypeScript) - GraphQL APIs (use
graphqlskill) - Simple REST APIs without shared types (use
openapi-codegeninstead) - Non-TypeScript projects
- Microservices with different languages
- Public APIs consumed by third parties (OpenAPI spec better)
Anti-Patterns
| Anti-Pattern | Why It's Bad | Solution |
|---|---|---|
| Sharing database entities as API types | Leaks implementation, tight coupling | Create separate DTOs/schemas |
| No runtime validation | Type safety only at compile time | Use Zod for runtime validation |
| Duplicating schemas between packages | Maintenance burden, drift risk | Use shared schema package in monorepo |
| Not versioning shared types | Breaking changes affect all consumers | Version shared package, use semver |
| Missing contract tests | Types match but behavior doesn't | Implement Pact or similar contract testing |
| Mixing type-safety approaches | Complexity, inconsistency | Choose one approach (tRPC, ts-rest, or Zod-OpenAPI) |
| No breaking change detection | Silent failures in production | Add schema diff checking in CI |
| Hardcoding types instead of generating | Manual sync burden | Generate from single source of truth |
Quick Troubleshooting
| Issue | Possible Cause | Solution |
|---|---|---|
| Type mismatches between FE/BE | Shared types not updated | Regenerate types, check imports |
| Runtime validation fails | Request doesn't match schema | Check request payload, update schema |
| Contract tests failing | API behavior changed | Update contract or fix API implementation |
| Circular dependency errors | Frontend importing backend code | Use separate shared types package |
| Breaking changes not detected | No schema diffing | Add schema versioning and diff tool |
| Schema generation fails | Invalid Zod schema | Check schema syntax, validate with Zod |
| OpenAPI spec out of sync | Manual spec edits | Generate spec from Zod schemas |
| Type inference not working | Wrong import or export | Verify type exports from shared package |
Reference Documentation
- Zod to OpenAPI
- ts-rest
- Contract Testing
Contract Testing Quick Reference
See Type-Safe API SKILL for core knowledge
Pact (Consumer-Driven Contracts)
Installation
npm install -D @pact-foundation/pactConsumer Test
import { Pact, Matchers } from '@pact-foundation/pact';
import path from 'path';
const { like, eachLike, regex } = Matchers;
const provider = new Pact({
consumer: 'Frontend',
provider: 'UserAPI',
port: 1234,
log: path.resolve(__dirname, 'logs', 'pact.log'),
dir: path.resolve(__dirname, 'pacts'),
});
describe('User API Contract', () => {
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
afterEach(() => provider.verify());
describe('GET /users/:id', () => {
it('returns user when exists', async () => {
await provider.addInteraction({
state: 'user with id 123 exists',
uponReceiving: 'a request to get user 123',
withRequest: {
method: 'GET',
path: '/users/123',
headers: {
Accept: 'application/json',
},
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: like({
id: '123',
name: 'John Doe',
email: 'john@example.com',
}),
},
});
const response = await fetch(`${provider.mockService.baseUrl}/users/123`);
const user = await response.json();
expect(user.id).toBe('123');
expect(user.name).toBe('John Doe');
});
it('returns 404 when not found', async () => {
await provider.addInteraction({
state: 'user with id 999 does not exist',
uponReceiving: 'a request to get user 999',
withRequest: {
method: 'GET',
path: '/users/999',
},
willRespondWith: {
status: 404,
body: like({
message: 'User not found',
}),
},
});
const response = await fetch(`${provider.mockService.baseUrl}/users/999`);
expect(response.status).toBe(404);
});
});
describe('POST /users', () => {
it('creates user', async () => {
await provider.addInteraction({
state: 'can create users',
uponReceiving: 'a request to create user',
withRequest: {
method: 'POST',
path: '/users',
headers: {
'Content-Type': 'application/json',
},
body: {
name: 'Jane Doe',
email: 'jane@example.com',
},
},
willRespondWith: {
status: 201,
body: like({
id: regex(/^[a-z0-9-]+$/, 'user-456'),
name: 'Jane Doe',
email: 'jane@example.com',
}),
},
});
const response = await fetch(`${provider.mockService.baseUrl}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Jane Doe', email: 'jane@example.com' }),
});
expect(response.status).toBe(201);
});
});
});Provider Verification
import { Verifier } from '@pact-foundation/pact';
describe('Provider Verification', () => {
it('validates the expectations of the consumer', async () => {
const verifier = new Verifier({
provider: 'UserAPI',
providerBaseUrl: 'http://localhost:3000',
pactUrls: [path.resolve(__dirname, 'pacts', 'frontend-userapi.json')],
stateHandlers: {
'user with id 123 exists': async () => {
await db.user.create({ id: '123', name: 'John Doe', email: 'john@example.com' });
},
'user with id 999 does not exist': async () => {
await db.user.deleteMany({ where: { id: '999' } });
},
'can create users': async () => {
// Setup for user creation
},
},
});
await verifier.verifyProvider();
});
});---
OpenAPI Contract Testing
With AJV
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import SwaggerParser from '@apidevtools/swagger-parser';
const ajv = new Ajv({ strict: false, allErrors: true });
addFormats(ajv);
let api: any;
beforeAll(async () => {
api = await SwaggerParser.dereference('./openapi.yaml');
});
describe('API Contract', () => {
it('GET /users/:id returns valid response', async () => {
const response = await fetch('http://localhost:3000/users/123');
const data = await response.json();
const schema = api.paths['/users/{id}'].get.responses['200'].content['application/json'].schema;
const validate = ajv.compile(schema);
const valid = validate(data);
expect(valid).toBe(true);
if (!valid) {
console.log(validate.errors);
}
});
});With Prism
# Install Prism
npm install -D @stoplight/prism-cli
# Mock server
npx prism mock openapi.yaml
# Validation proxy
npx prism proxy openapi.yaml http://localhost:3000// Test against Prism proxy
describe('API Contract with Prism', () => {
it('validates request/response', async () => {
// Prism validates automatically
const response = await fetch('http://localhost:4010/users/123');
expect(response.ok).toBe(true);
});
});---
Zod Runtime Validation
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
describe('API Response Validation', () => {
it('validates response against schema', async () => {
const response = await fetch('/api/users/123');
const data = await response.json();
const result = UserSchema.safeParse(data);
expect(result.success).toBe(true);
if (!result.success) {
console.log(result.error.issues);
}
});
});---
CI Integration
# .github/workflows/contract-test.yml
name: Contract Tests
on: [push, pull_request]
jobs:
consumer-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run test:contract
- uses: actions/upload-artifact@v4
with:
name: pacts
path: pacts/
provider-verification:
needs: consumer-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: pacts
path: pacts/
- run: npm ci
- run: npm run start:test &
- run: npm run verify:pactsPackage Scripts
{
"scripts": {
"test:contract": "jest --config jest.contract.config.js",
"verify:pacts": "jest --config jest.pact-verify.config.js",
"pact:publish": "pact-broker publish ./pacts --consumer-app-version=$npm_package_version"
}
}ts-rest Quick Reference
See Type-Safe API SKILL for core knowledge
Installation
npm install @ts-rest/core
npm install @ts-rest/next # Next.js
npm install @ts-rest/express # Express
npm install @ts-rest/react-query # React QueryDefine Contract
// contracts/index.ts
import { initContract } from '@ts-rest/core';
import { z } from 'zod';
const c = initContract();
export const contract = c.router({
users: {
get: {
method: 'GET',
path: '/users/:id',
pathParams: z.object({ id: z.string() }),
responses: {
200: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
404: z.object({ message: z.string() }),
},
},
create: {
method: 'POST',
path: '/users',
body: z.object({
name: z.string(),
email: z.string().email(),
}),
responses: {
201: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
},
},
list: {
method: 'GET',
path: '/users',
query: z.object({
page: z.coerce.number().optional(),
limit: z.coerce.number().optional(),
}),
responses: {
200: z.array(z.object({
id: z.string(),
name: z.string(),
})),
},
},
},
});Server - Next.js
// pages/api/[...ts-rest].ts
import { createNextRoute, createNextRouter } from '@ts-rest/next';
import { contract } from '../../contracts';
const router = createNextRouter(contract, {
users: {
get: async ({ params }) => {
const user = await db.user.findUnique({ where: { id: params.id } });
if (!user) {
return { status: 404, body: { message: 'Not found' } };
}
return { status: 200, body: user };
},
create: async ({ body }) => {
const user = await db.user.create({ data: body });
return { status: 201, body: user };
},
list: async ({ query }) => {
const users = await db.user.findMany({
take: query.limit ?? 10,
skip: ((query.page ?? 1) - 1) * (query.limit ?? 10),
});
return { status: 200, body: users };
},
},
});
export default createNextRoute(contract, router);Server - Express
import express from 'express';
import { createExpressEndpoints } from '@ts-rest/express';
import { contract } from './contracts';
const app = express();
app.use(express.json());
createExpressEndpoints(contract, {
users: {
get: async ({ params }) => {
const user = await db.user.findUnique({ where: { id: params.id } });
if (!user) {
return { status: 404, body: { message: 'Not found' } };
}
return { status: 200, body: user };
},
create: async ({ body }) => {
const user = await db.user.create({ data: body });
return { status: 201, body: user };
},
list: async ({ query }) => {
const users = await db.user.findMany();
return { status: 200, body: users };
},
},
}, app);
app.listen(3000);Client
import { initClient } from '@ts-rest/core';
import { contract } from './contracts';
const client = initClient(contract, {
baseUrl: 'https://api.example.com',
baseHeaders: {
Authorization: `Bearer ${token}`,
},
});
// Typed requests
const { body: user, status } = await client.users.get({
params: { id: '123' },
});
const { body: newUser } = await client.users.create({
body: { name: 'John', email: 'john@example.com' },
});
const { body: users } = await client.users.list({
query: { page: 1, limit: 10 },
});React Query
import { initQueryClient } from '@ts-rest/react-query';
import { contract } from './contracts';
const client = initQueryClient(contract, {
baseUrl: '/api',
});
// In component
function UserProfile({ id }: { id: string }) {
const { data, isLoading, error } = client.users.get.useQuery(
['user', id],
{ params: { id } }
);
const createMutation = client.users.create.useMutation();
const handleCreate = () => {
createMutation.mutate({
body: { name: 'New User', email: 'new@example.com' },
});
};
if (isLoading) return <Spinner />;
if (error) return <Error />;
return <div>{data?.body.name}</div>;
}Response Handling
const result = await client.users.get({ params: { id: '123' } });
if (result.status === 200) {
console.log(result.body.name); // Typed as User
} else if (result.status === 404) {
console.log(result.body.message); // Typed as { message: string }
}Middleware
const client = initClient(contract, {
baseUrl: 'https://api.example.com',
api: async ({ path, method, headers, body }) => {
// Custom fetch logic
const response = await fetch(path, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
return {
status: response.status,
body: await response.json(),
headers: response.headers,
};
},
});Generate OpenAPI
import { generateOpenApi } from '@ts-rest/open-api';
import { contract } from './contracts';
const openApiDocument = generateOpenApi(contract, {
info: {
title: 'My API',
version: '1.0.0',
},
});Zod to OpenAPI Quick Reference
See Type-Safe API SKILL for core knowledge
Installation
npm install @asteasolutions/zod-to-openapi zodSetup
import { z } from 'zod';
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
extendZodWithOpenApi(z);Schema Definition
// Add OpenAPI metadata to schemas
const UserSchema = z.object({
id: z.string().uuid().openapi({ example: 'user_123' }),
name: z.string().min(1).max(100).openapi({ example: 'John Doe' }),
email: z.string().email().openapi({ example: 'john@example.com' }),
role: z.enum(['user', 'admin']).default('user'),
createdAt: z.date(),
}).openapi('User');
const CreateUserSchema = UserSchema
.omit({ id: true, createdAt: true })
.openapi('CreateUser');
const UpdateUserSchema = CreateUserSchema
.partial()
.openapi('UpdateUser');
// Export types
export type User = z.infer<typeof UserSchema>;
export type CreateUser = z.infer<typeof CreateUserSchema>;Registry Setup
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
const registry = new OpenAPIRegistry();
// Register schemas
registry.register('User', UserSchema);
registry.register('CreateUser', CreateUserSchema);Register Endpoints
// GET endpoint
registry.registerPath({
method: 'get',
path: '/users/{id}',
summary: 'Get user by ID',
tags: ['Users'],
request: {
params: z.object({
id: z.string().uuid(),
}),
},
responses: {
200: {
description: 'User found',
content: {
'application/json': { schema: UserSchema },
},
},
404: {
description: 'User not found',
content: {
'application/json': {
schema: z.object({
message: z.string(),
}),
},
},
},
},
});
// POST endpoint
registry.registerPath({
method: 'post',
path: '/users',
summary: 'Create user',
tags: ['Users'],
request: {
body: {
content: {
'application/json': { schema: CreateUserSchema },
},
},
},
responses: {
201: {
description: 'User created',
content: {
'application/json': { schema: UserSchema },
},
},
400: {
description: 'Validation error',
},
},
});
// GET with query params
registry.registerPath({
method: 'get',
path: '/users',
summary: 'List users',
tags: ['Users'],
request: {
query: z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(100).default(10),
status: z.enum(['active', 'inactive']).optional(),
}),
},
responses: {
200: {
description: 'List of users',
content: {
'application/json': {
schema: z.object({
data: z.array(UserSchema),
total: z.number(),
}),
},
},
},
},
});Generate Document
import { OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';
const generator = new OpenApiGeneratorV3(registry.definitions);
const openApiDocument = generator.generateDocument({
openapi: '3.0.0',
info: {
title: 'User API',
version: '1.0.0',
description: 'API for managing users',
},
servers: [
{ url: 'https://api.example.com', description: 'Production' },
{ url: 'http://localhost:3000', description: 'Development' },
],
});
// Export as JSON
console.log(JSON.stringify(openApiDocument, null, 2));Security Schemes
registry.registerComponent('securitySchemes', 'bearerAuth', {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
});
// Protected endpoint
registry.registerPath({
method: 'get',
path: '/users/me',
summary: 'Get current user',
security: [{ bearerAuth: [] }],
responses: {
200: {
content: { 'application/json': { schema: UserSchema } },
},
},
});Common Patterns
// Pagination schema
const PaginationSchema = z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(100).default(10),
});
// Paginated response
const createPaginatedResponse = <T extends z.ZodTypeAny>(schema: T) =>
z.object({
data: z.array(schema),
pagination: z.object({
page: z.number(),
limit: z.number(),
total: z.number(),
totalPages: z.number(),
}),
});
// Error response
const ErrorSchema = z.object({
code: z.string(),
message: z.string(),
details: z.array(z.object({
field: z.string(),
message: z.string(),
})).optional(),
}).openapi('Error');Save to File
import fs from 'fs';
fs.writeFileSync(
'./openapi.json',
JSON.stringify(openApiDocument, null, 2)
);Package Script
{
"scripts": {
"generate:openapi": "tsx scripts/generate-openapi.ts"
}
}