兩個方向:Code-First vs Contract-First
Code-First:先寫 code,從 code annotation 生成 OpenAPI 文件。
// NestJS 的 Code-First
@ApiOperation({ summary: '建立用戶' })
@ApiResponse({ status: 201, type: UserResponse })
@Post('/users')
async createUser(@Body() dto: CreateUserDto) {
return this.userService.create(dto);
}Swagger UI 從這些 annotation 自動生成——文件和 code 住在一起,不容易漏更新。
Contract-First:先寫 OpenAPI spec(YAML/JSON),再從 spec 生成 code skeleton 或型別定義。
# openapi.yaml 先寫
paths:
/users:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'怎麼選:
- Code-First:適合小團隊、已有 code 要補文件、前後端是同一個人
- Contract-First:適合前後端分離開發(spec 先定好,前後端並行開發)、多個 client 消費(mobile、web、第三方)、API 設計本身就是產品(公開 API)
Express + Code-First(swagger-jsdoc)
Express 沒有內建文件生成,用 JSDoc 風格的 annotation:
import swaggerJSDoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: { title: 'My API', version: '1.0.0' },
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
},
apis: ['./src/routes/**/*.ts'], // 掃描這些檔案裡的 JSDoc
};
const swaggerSpec = swaggerJSDoc(swaggerOptions);
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));/**
* @openapi
* /api/users:
* post:
* summary: 建立用戶
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateUserRequest'
* responses:
* 201:
* description: 建立成功
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserResponse'
* 400:
* description: 驗證錯誤
*/
router.post('/users', validate(createUserSchema), createUserHandler);缺點:annotation 是字串,不能靜態型別檢查。Zod schema 和 OpenAPI schema 要各自維護,容易不同步。
Zod → OpenAPI 自動生成(更好的做法)
用 zod-to-openapi 或 @asteasolutions/zod-to-openapi,讓 Zod schema 同時成為 OpenAPI schema:
import { OpenAPIRegistry, OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';
const registry = new OpenAPIRegistry();
// 定義 schema(同時用在 validation 和文件)
const CreateUserSchema = registry.register(
'CreateUserRequest',
z.object({
name: z.string().min(1).max(100).openapi({ example: 'Alice' }),
email: z.string().email().openapi({ example: 'alice@example.com' }),
role: z.enum(['admin', 'viewer']).openapi({ example: 'viewer' }),
})
);
const UserResponseSchema = registry.register(
'UserResponse',
z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().datetime(),
})
);
// 定義 route
registry.registerPath({
method: 'post',
path: '/users',
summary: '建立用戶',
tags: ['Users'],
security: [{ bearerAuth: [] }],
request: { body: { content: { 'application/json': { schema: CreateUserSchema } } } },
responses: {
201: {
description: '建立成功',
content: { 'application/json': { schema: UserResponseSchema } },
},
},
});
// 生成 OpenAPI spec
const generator = new OpenApiGeneratorV3(registry.definitions);
const spec = generator.generateDocument({
openapi: '3.0.0',
info: { title: 'My API', version: '1.0.0' },
});好處:Zod schema 改了,OpenAPI spec 自動跟著改,永遠同步。
Contract-First 工作流程
1. 前後端一起設計 → 寫 openapi.yaml
2. 前端從 spec 生成型別(openapi-typescript)
3. 後端從 spec 生成路由骨架或用作 request validation
4. 前後端並行開發,用 mock server(Prism)模擬 API
5. API 實作完成後接上真實後端
前端從 OpenAPI spec 生成型別:
npx openapi-typescript openapi.yaml -o src/types/api.d.ts生成的型別:
// 自動生成,不要手動改
export interface paths {
'/users': {
post: {
requestBody: { content: { 'application/json': components['schemas']['CreateUserRequest'] } };
responses: {
201: { content: { 'application/json': components['schemas']['UserResponse'] } };
};
};
};
}後端用 spec 驗 request(express-openapi-validator):
import OpenApiValidator from 'express-openapi-validator';
app.use(OpenApiValidator.middleware({
apiSpec: './openapi.yaml',
validateRequests: true, // request 不符合 spec → 400
validateResponses: true, // response 不符合 spec → 500(開發時找 bug 用)
}));這樣一份 spec 同時驗前端送來的 request 和後端回傳的 response,任何人的實作出軌都會在開發時被抓到。
Swagger UI 的實際設定
// 只在非 production 開放 Swagger UI
if (process.env.NODE_ENV !== 'production') {
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
swaggerOptions: {
persistAuthorization: true, // 重整頁面不清 auth token
},
}));
}
// 輸出 spec(讓 CI 可以 diff)
app.get('/docs/spec.json', (req, res) => res.json(swaggerSpec));Production 要不要開放 Swagger UI?
- 公開 API(給第三方開發者):要,這是文件本身
- 內部 API:不要——暴露所有 endpoint 細節是安全風險
Versioning + OpenAPI
API 版本和 OpenAPI spec 版本要對應:
/docs/v1/spec.json → v1 的 spec
/docs/v2/spec.json → v2 的 spec
或者用一份 spec,用 x-api-version extension 標記哪些 endpoint 在哪個版本出現。
舊版 spec 不要刪——外部開發者可能還在查舊版文件。
