type-safe-api

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Type-Safe API Core Knowledge

Type-Safe API 核心知识

Deep Knowledge: Use
mcp__documentation__fetch_docs
with technology:
type-safe-api
for comprehensive documentation.
深度参考:使用
mcp__documentation__fetch_docs
工具并指定技术类型为
type-safe-api
,可获取完整文档。

Zod to OpenAPI

Zod 转 OpenAPI

Generate OpenAPI specs from Zod schemas for type-first development.
bash
npm install @asteasolutions/zod-to-openapi zod
从Zod Schema生成OpenAPI规范,实现类型优先的开发模式。
bash
npm install @asteasolutions/zod-to-openapi zod

Define Schemas

定义Schema

typescript
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>;
typescript
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

生成OpenAPI文档

typescript
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' }],
});

typescript
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)

ts-rest(契约优先)

Type-safe REST API contracts shared between client and server.
bash
npm install @ts-rest/core
npm install @ts-rest/next        # For Next.js
npm install @ts-rest/react-query # For React Query
客户端与服务器之间共享的类型安全REST API契约。
bash
npm install @ts-rest/core
npm install @ts-rest/next        # For Next.js
npm install @ts-rest/react-query # For React Query

Define Contract

定义契约

typescript
// 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(),
      })),
    },
  },
});
typescript
// 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)

服务器实现(Next.js)

typescript
// 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);
typescript
// 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

客户端使用

typescript
// 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' },
});
typescript
// 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

React Query集成

typescript
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>;
}

typescript
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)

Zodios(类型安全REST客户端)

bash
npm install @zodios/core zod
npm install @zodios/react # For React hooks
bash
npm install @zodios/core zod
npm install @zodios/react # For React hooks

Define API

定义API

typescript
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);
typescript
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

客户端使用

typescript
// 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',
});

typescript
// 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

使用Pact

bash
npm install -D @pact-foundation/pact
typescript
import { 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');
  });
});

bash
npm install -D @pact-foundation/pact
typescript
import { 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)

共享类型策略(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.json
packages/
├── api-contracts/      # 共享契约
│   ├── src/
│   │   ├── schemas.ts  # Zod schema
│   │   ├── types.ts    # TypeScript类型
│   │   └── contract.ts # ts-rest契约
│   └── package.json
├── backend/
│   ├── src/
│   │   └── routes/     # 实现契约
│   └── package.json
└── frontend/
    ├── src/
    │   └── api/        # 使用契约
    └── package.json

Breaking Change Detection

破坏性变更检测

typescript
// 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);
}
typescript
// 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
  • Monorepo中包含共享Schema包
  • 从Schema生成OpenAPI规范
  • 服务间配置契约测试
  • CI中集成破坏性变更检测
  • 自动生成类型
  • 边界处配置运行时校验
  • 契约中包含错误类型
  • 定义版本化策略

When NOT to Use This Skill

不适用场景

  • tRPC projects (use
    trpc
    skill - simpler for full-stack TypeScript)
  • GraphQL APIs (use
    graphql
    skill)
  • Simple REST APIs without shared types (use
    openapi-codegen
    instead)
  • Non-TypeScript projects
  • Microservices with different languages
  • Public APIs consumed by third parties (OpenAPI spec better)
  • tRPC项目(请使用
    trpc
    方案,对全栈TypeScript更友好)
  • GraphQL API(请使用
    graphql
    方案)
  • 无需共享类型的简单REST API(请改用
    openapi-codegen
  • 非TypeScript项目
  • 使用不同语言的微服务
  • 供第三方调用的公开API(OpenAPI规范更合适)

Anti-Patterns

反模式

Anti-PatternWhy It's BadSolution
Sharing database entities as API typesLeaks implementation, tight couplingCreate separate DTOs/schemas
No runtime validationType safety only at compile timeUse Zod for runtime validation
Duplicating schemas between packagesMaintenance burden, drift riskUse shared schema package in monorepo
Not versioning shared typesBreaking changes affect all consumersVersion shared package, use semver
Missing contract testsTypes match but behavior doesn'tImplement Pact or similar contract testing
Mixing type-safety approachesComplexity, inconsistencyChoose one approach (tRPC, ts-rest, or Zod-OpenAPI)
No breaking change detectionSilent failures in productionAdd schema diff checking in CI
Hardcoding types instead of generatingManual sync burdenGenerate from single source of truth
反模式危害解决方案
将数据库实体作为API类型共享暴露实现细节,导致强耦合创建独立的DTO/Schema
无运行时校验仅在编译时保证类型安全使用Zod进行运行时校验
在多个包中重复定义Schema维护成本高,存在类型漂移风险在Monorepo中使用共享Schema包
不对共享类型进行版本化破坏性变更会影响所有消费者对共享包进行版本化,使用语义化版本控制
缺少契约测试类型匹配但行为不一致实现Pact或类似的契约测试
混合使用多种类型安全方案复杂度高,一致性差选择一种方案(tRPC、ts-rest或Zod-OpenAPI)
无破坏性变更检测生产环境中出现静默故障在CI中添加Schema差异检查
硬编码类型而非自动生成手动同步成本高从单一可信源生成类型

Quick Troubleshooting

快速故障排查

IssuePossible CauseSolution
Type mismatches between FE/BEShared types not updatedRegenerate types, check imports
Runtime validation failsRequest doesn't match schemaCheck request payload, update schema
Contract tests failingAPI behavior changedUpdate contract or fix API implementation
Circular dependency errorsFrontend importing backend codeUse separate shared types package
Breaking changes not detectedNo schema diffingAdd schema versioning and diff tool
Schema generation failsInvalid Zod schemaCheck schema syntax, validate with Zod
OpenAPI spec out of syncManual spec editsGenerate spec from Zod schemas
Type inference not workingWrong import or exportVerify type exports from shared package
问题可能原因解决方案
前后端类型不匹配共享类型未更新重新生成类型,检查导入路径
运行时校验失败请求不符合Schema检查请求 payload,更新Schema
契约测试失败API行为变更更新契约或修复API实现
循环依赖错误前端导入后端代码使用独立的共享类型包
未检测到破坏性变更未配置Schema差异检查添加Schema版本化和差异工具
Schema生成失败Zod Schema无效检查Schema语法,使用Zod验证
OpenAPI规范不同步手动修改了规范从Zod Schema自动生成规范
类型推断不生效导入或导出错误验证共享包中的类型导出

Reference Documentation

参考文档

  • Zod to OpenAPI
  • ts-rest
  • Contract Testing
  • Zod to OpenAPI
  • ts-rest
  • Contract Testing