backend-trpc-openapi
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesetRPC + OpenAPI Integration
tRPC + OpenAPI 集成
Overview
概述
Generate REST endpoints and OpenAPI documentation from your tRPC routers. Get the best of both worlds: type-safe internal API with tRPC, REST/Swagger for external consumers.
Package: (active fork of archived )
Requirements: tRPC v11+, Zod
trpc-to-openapitrpc-openapiRequirements: tRPC v11+, Zod
Key Benefit: Single source of truth — define once in tRPC, expose as both RPC and REST.
从你的tRPC路由器生成REST端点和OpenAPI文档。兼得两者优势:使用tRPC实现类型安全的内部API,为外部消费者提供REST/Swagger支持。
包:(已归档的的活跃分支)
要求:tRPC v11+、Zod
trpc-to-openapitrpc-openapi要求:tRPC v11+、Zod
核心优势:单一事实来源——在tRPC中定义一次,同时以RPC和REST方式暴露。
When to Use This Skill
何时使用该方案
✅ Use tRPC + OpenAPI when:
- Internal apps use tRPC, but need REST for third parties
- Need Swagger/OpenAPI documentation
- Mobile apps (non-React Native) need REST endpoints
- Microservices with mixed languages need interop
- Public API requires REST standard
❌ Skip OpenAPI layer when:
- All clients are TypeScript (pure tRPC is better)
- Internal-only APIs
- No documentation requirements
✅ 在以下场景使用tRPC + OpenAPI:
- 内部应用使用tRPC,但需要为第三方提供REST接口
- 需要Swagger/OpenAPI文档
- 移动应用(非React Native)需要REST端点
- 多语言微服务需要互操作
- 公开API要求遵循REST标准
❌ 在以下场景跳过OpenAPI层:
- 所有客户端均为TypeScript(纯tRPC更合适)
- 仅内部使用的API
- 无文档需求
Quick Start
快速开始
Installation
安装
bash
undefinedbash
undefinedNOTE: trpc-openapi is ARCHIVED, use active fork
注意:trpc-openapi已归档,请使用活跃分支
npm install trpc-to-openapi swagger-ui-express
npm install -D @types/swagger-ui-express
undefinednpm install trpc-to-openapi swagger-ui-express
npm install -D @types/swagger-ui-express
undefinedSetup tRPC with OpenAPI Meta
配置带OpenAPI元数据的tRPC
typescript
// src/server/trpc.ts
import { initTRPC } from '@trpc/server';
import { OpenApiMeta } from 'trpc-to-openapi';
const t = initTRPC
.context<Context>()
.meta<OpenApiMeta>() // ← Enable OpenAPI metadata
.create();
export const router = t.router;
export const publicProcedure = t.procedure;typescript
// src/server/trpc.ts
import { initTRPC } from '@trpc/server';
import { OpenApiMeta } from 'trpc-to-openapi';
const t = initTRPC
.context<Context>()
.meta<OpenApiMeta>() // ← 启用OpenAPI元数据
.create();
export const router = t.router;
export const publicProcedure = t.procedure;Define Procedures with OpenAPI Metadata
定义带OpenAPI元数据的程序
typescript
// src/server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
});
export const userRouter = router({
// GET /api/users/{id}
getById: publicProcedure
.meta({
openapi: {
method: 'GET',
path: '/users/{id}',
tags: ['Users'],
summary: 'Get user by ID',
description: 'Retrieves a single user by their unique identifier',
},
})
.input(z.object({ id: z.string() }))
.output(UserSchema)
.query(async ({ input, ctx }) => {
return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
}),
// GET /api/users?limit=10&cursor=xxx
list: publicProcedure
.meta({
openapi: {
method: 'GET',
path: '/users',
tags: ['Users'],
summary: 'List users',
},
})
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
}))
.output(z.object({
items: z.array(UserSchema),
nextCursor: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
// pagination logic
}),
// POST /api/users (protected)
create: protectedProcedure
.meta({
openapi: {
method: 'POST',
path: '/users',
tags: ['Users'],
summary: 'Create user',
protect: true, // ← Marks as requiring auth in docs
},
})
.input(z.object({
email: z.string().email(),
name: z.string().min(2),
}))
.output(UserSchema)
.mutation(async ({ input, ctx }) => {
return ctx.db.user.create({ data: input });
}),
// PUT /api/users/{id}
update: protectedProcedure
.meta({
openapi: {
method: 'PUT',
path: '/users/{id}',
tags: ['Users'],
protect: true,
},
})
.input(z.object({
id: z.string(),
name: z.string().optional(),
email: z.string().email().optional(),
}))
.output(UserSchema)
.mutation(async ({ input, ctx }) => {
const { id, ...data } = input;
return ctx.db.user.update({ where: { id }, data });
}),
// DELETE /api/users/{id}
delete: protectedProcedure
.meta({
openapi: {
method: 'DELETE',
path: '/users/{id}',
tags: ['Users'],
protect: true,
},
})
.input(z.object({ id: z.string() }))
.output(z.object({ success: z.boolean() }))
.mutation(async ({ input, ctx }) => {
await ctx.db.user.delete({ where: { id: input.id } });
return { success: true };
}),
});typescript
// src/server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
});
export const userRouter = router({
// GET /api/users/{id}
getById: publicProcedure
.meta({
openapi: {
method: 'GET',
path: '/users/{id}',
tags: ['Users'],
summary: '通过ID获取用户',
description: '通过唯一标识符检索单个用户',
},
})
.input(z.object({ id: z.string() }))
.output(UserSchema)
.query(async ({ input, ctx }) => {
return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
}),
// GET /api/users?limit=10&cursor=xxx
list: publicProcedure
.meta({
openapi: {
method: 'GET',
path: '/users',
tags: ['Users'],
summary: '列出用户',
},
})
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
}))
.output(z.object({
items: z.array(UserSchema),
nextCursor: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
// 分页逻辑
}),
// POST /api/users(受保护)
create: protectedProcedure
.meta({
openapi: {
method: 'POST',
path: '/users',
tags: ['Users'],
summary: '创建用户',
protect: true, // ← 在文档中标记为需要认证
},
})
.input(z.object({
email: z.string().email(),
name: z.string().min(2),
}))
.output(UserSchema)
.mutation(async ({ input, ctx }) => {
return ctx.db.user.create({ data: input });
}),
// PUT /api/users/{id}
update: protectedProcedure
.meta({
openapi: {
method: 'PUT',
path: '/users/{id}',
tags: ['Users'],
protect: true,
},
})
.input(z.object({
id: z.string(),
name: z.string().optional(),
email: z.string().email().optional(),
}))
.output(UserSchema)
.mutation(async ({ input, ctx }) => {
const { id, ...data } = input;
return ctx.db.user.update({ where: { id }, data });
}),
// DELETE /api/users/{id}
delete: protectedProcedure
.meta({
openapi: {
method: 'DELETE',
path: '/users/{id}',
tags: ['Users'],
protect: true,
},
})
.input(z.object({ id: z.string() }))
.output(z.object({ success: z.boolean() }))
.mutation(async ({ input, ctx }) => {
await ctx.db.user.delete({ where: { id: input.id } });
return { success: true };
}),
});Generate OpenAPI Document
生成OpenAPI文档
typescript
// src/server/openapi.ts
import { generateOpenApiDocument } from 'trpc-to-openapi';
import { appRouter } from './routers/_app';
export const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'My API',
version: '1.0.0',
baseUrl: process.env.API_URL || 'http://localhost:3000/api',
description: 'REST API documentation',
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
});typescript
// src/server/openapi.ts
import { generateOpenApiDocument } from 'trpc-to-openapi';
import { appRouter } from './routers/_app';
export const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'My API',
version: '1.0.0',
baseUrl: process.env.API_URL || 'http://localhost:3000/api',
description: 'REST API文档',
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
});Serve REST Endpoints + Swagger UI
提供REST端点 + Swagger UI
typescript
// src/server/index.ts
import express from 'express';
import cors from 'cors';
import swaggerUi from 'swagger-ui-express';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { createOpenApiExpressMiddleware } from 'trpc-to-openapi';
import { appRouter } from './routers/_app';
import { createContext } from './context';
import { openApiDocument } from './openapi';
const app = express();
app.use(cors());
app.use(express.json());
// tRPC endpoint (for TypeScript clients)
app.use('/trpc', createExpressMiddleware({
router: appRouter,
createContext,
}));
// REST/OpenAPI endpoints (for external clients)
app.use('/api', createOpenApiExpressMiddleware({
router: appRouter,
createContext,
}));
// Swagger UI documentation
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument));
// OpenAPI JSON spec
app.get('/openapi.json', (req, res) => {
res.json(openApiDocument);
});
app.listen(3000, () => {
console.log('Server: http://localhost:3000');
console.log('tRPC: http://localhost:3000/trpc');
console.log('REST: http://localhost:3000/api');
console.log('Docs: http://localhost:3000/docs');
});typescript
// src/server/index.ts
import express from 'express';
import cors from 'cors';
import swaggerUi from 'swagger-ui-express';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { createOpenApiExpressMiddleware } from 'trpc-to-openapi';
import { appRouter } from './routers/_app';
import { createContext } from './context';
import { openApiDocument } from './openapi';
const app = express();
app.use(cors());
app.use(express.json());
// tRPC端点(面向TypeScript客户端)
app.use('/trpc', createExpressMiddleware({
router: appRouter,
createContext,
}));
// REST/OpenAPI端点(面向外部客户端)
app.use('/api', createOpenApiExpressMiddleware({
router: appRouter,
createContext,
}));
// Swagger UI文档
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument));
// OpenAPI JSON规范
app.get('/openapi.json', (req, res) => {
res.json(openApiDocument);
});
app.listen(3000, () => {
console.log('服务器:http://localhost:3000');
console.log('tRPC:http://localhost:3000/trpc');
console.log('REST:http://localhost:3000/api');
console.log('文档:http://localhost:3000/docs');
});URL Parameter Mapping
URL参数映射
typescript
// Path parameters use {param} syntax
.meta({
openapi: {
method: 'GET',
path: '/users/{id}/posts/{postId}',
},
})
.input(z.object({
id: z.string(), // ← Maps to {id}
postId: z.string(), // ← Maps to {postId}
}))
// Query parameters are auto-mapped for GET
.meta({
openapi: {
method: 'GET',
path: '/users',
},
})
.input(z.object({
limit: z.number(), // ← ?limit=10
search: z.string(), // ← &search=foo
}))typescript
// 路径参数使用{param}语法
.meta({
openapi: {
method: 'GET',
path: '/users/{id}/posts/{postId}',
},
})
.input(z.object({
id: z.string(), // ← 映射到{id}
postId: z.string(), // ← 映射到{postId}
}))
// GET请求的查询参数会自动映射
.meta({
openapi: {
method: 'GET',
path: '/users',
},
})
.input(z.object({
limit: z.number(), // ← ?limit=10
search: z.string(), // ← &search=foo
}))When to Expose OpenAPI
何时暴露OpenAPI
| Scenario | Recommendation |
|---|---|
| Internal TypeScript clients | Pure tRPC |
| Third-party integrations | tRPC + OpenAPI |
| Public API documentation | tRPC + OpenAPI |
| Mobile apps (non-React Native) | tRPC + OpenAPI |
| Microservices (mixed languages) | OpenAPI |
| 场景 | 推荐方案 |
|---|---|
| 内部TypeScript客户端 | 纯tRPC |
| 第三方集成 | tRPC + OpenAPI |
| 公开API文档 | tRPC + OpenAPI |
| 移动应用(非React Native) | tRPC + OpenAPI |
| 多语言微服务 | OpenAPI |
Rules
规则
Do ✅
建议✅
- Add schema for OpenAPI response types
.output() - Use descriptive and
summarydescription - Group related endpoints with
tags - Mark protected routes with
protect: true - Use path parameters for resource identifiers
- 为OpenAPI响应类型添加schema
.output() - 使用描述性的和
summarydescription - 使用对相关端点进行分组
tags - 用标记受保护的路由
protect: true - 对资源标识符使用路径参数
Avoid ❌
避免❌
- Exposing all procedures (only add meta to public ones)
- Missing output schemas (breaks OpenAPI generation)
- Inconsistent path naming conventions
- Skipping authentication markers
- 暴露所有程序(仅为公开程序添加元数据)
- 缺失输出schema(会导致OpenAPI生成失败)
- 路径命名约定不一致
- 跳过认证标记
OpenAPI Metadata Reference
OpenAPI元数据参考
typescript
.meta({
openapi: {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
path: '/resource/{id}',
tags: ['Category'],
summary: 'Short description',
description: 'Detailed description',
protect: boolean, // Requires auth
deprecated: boolean, // Mark as deprecated
requestHeaders: z.object(), // Custom headers
responseHeaders: z.object(),
contentTypes: ['application/json'],
},
})typescript
.meta({
openapi: {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
path: '/resource/{id}',
tags: ['Category'],
summary: '简短描述',
description: '详细描述',
protect: boolean, // 需要认证
deprecated: boolean, // 标记为已弃用
requestHeaders: z.object(), // 自定义请求头
responseHeaders: z.object(),// 自定义响应头
contentTypes: ['application/json'],
},
})Troubleshooting
故障排除
yaml
"OpenAPI generation fails":
→ Ensure all procedures with meta have .output()
→ Check Zod schemas are serializable
→ Verify path parameters match input schema
"REST endpoint returns 404":
→ Check path matches exactly (case-sensitive)
→ Verify HTTP method matches
→ Ensure createOpenApiExpressMiddleware is mounted
"Auth not working on REST":
→ Check Authorization header format
→ Verify createContext extracts token
→ Match auth middleware with tRPC setup
"Swagger UI empty":
→ Check openApiDocument is generated
→ Verify /openapi.json returns valid spec
→ Check console for generation errorsyaml
"OpenAPI生成失败":
→ 确保所有带元数据的程序都有.output()
→ 检查Zod schema是否可序列化
→ 验证路径参数与输入schema匹配
"REST端点返回404":
→ 检查路径是否完全匹配(区分大小写)
→ 验证HTTP方法是否匹配
→ 确保已挂载createOpenApiExpressMiddleware
"REST接口认证不生效":
→ 检查Authorization头格式
→ 验证createContext是否正确提取token
→ 确保认证中间件与tRPC配置一致
"Swagger UI为空":
→ 检查是否已生成openApiDocument
→ 验证/openapi.json返回有效的规范
→ 检查控制台是否有生成错误File Structure
文件结构
src/server/
├── trpc.ts # tRPC with OpenApiMeta
├── openapi.ts # OpenAPI document generation
├── context.ts # Shared context
├── index.ts # Express server
└── routers/
├── _app.ts # Root router
└── user.ts # Procedures with openapi metasrc/server/
├── trpc.ts # 带OpenApiMeta的tRPC配置
├── openapi.ts # OpenAPI文档生成
├── context.ts # 共享上下文
├── index.ts # Express服务器
└── routers/
├── _app.ts # 根路由器
└── user.ts # 带openapi元数据的程序References
参考链接
- https://github.com/mcampa/trpc-to-openapi — Active fork documentation
- https://swagger.io/specification/ — OpenAPI spec
- https://swagger.io/tools/swagger-ui/ — Swagger UI
- https://github.com/mcampa/trpc-to-openapi — 活跃分支文档
- https://swagger.io/specification/ — OpenAPI规范
- https://swagger.io/tools/swagger-ui/ — Swagger UI