API 文档(Swagger / OpenAPI)
Finch 内置了 OpenAPI 文档系统。你使用 ApiDoc 描述每个 API 路由,Finch 会生成机器可读的 OpenAPI 3.1 JSON 规范,以及一个 Swagger UI,让开发者可以在浏览器中浏览并测试你的 API。
设置 API 文档需要三个步骤:
- 创建一个
ApiController实例。 - 注册文档路由(OpenAPI JSON 输出和 Swagger UI)。
- 将
ApiDoc对象附加到应在文档中显示的路由上。
第 1 步 — 创建 ApiController
ApiController 会读取应用中的所有路由并生成 OpenAPI 规范:
final apiController = ApiController(
title: 'My App API',
app: app,
security: 'apiKey', // 可选 —— Bearer 安全方案在文档中显示的名称,默认为 'apiKey'
);
第 2 步 — 注册文档路由
向你的路由中添加两个路由:一个用于输出原始 OpenAPI JSON,另一个用于 Swagger UI:
[
// 返回 OpenAPI JSON 规范 —— 供 Swagger UI 和 API 客户端使用
FinchRoute(
key: 'root.api.docs',
path: 'api/docs',
index: apiController.indexPublic,
),
// Swagger UI 页面 —— 传入 JSON 规范的地址
FinchRoute(
key: 'root.swagger',
path: 'swagger',
index: () => apiController.swagger(rq.url('api/docs')),
),
];
在浏览器中打开 /swagger 即可查看交互式文档。
默认情况下,Swagger UI(以及普通的
index()JSON 接口)仅在本地调试模式(isLocalDebug: true)下可访问。indexPublic()是index(showPublic: true)的简写,因此上面的 JSON 规范路由本身就是公开的——如果还想让apiController.swagger(...)无论是否处于调试模式都可公开访问,请显式向它传入showPublic: true。
只有路径以 /api 开头、并且附加了 apiDoc 的路由,才会被包含在生成的规范中。
第 3 步 — 为路由定义 ApiDoc
每个路由都可以有一个 ApiDoc,用来描述该路由的作用、接受的参数/请求体,以及返回的响应。通过 apiDoc 属性将其附加到 FinchRoute 上。
FinchRoute(
key: 'api.books.list',
path: 'api/books',
methods: Methods.ONLY_GET,
index: booksController.list,
apiDoc: ApiDoc(
get: ApiDoc(
description: 'Returns a paginated list of books.',
parameters: [
ApiParameter<int>(
'page',
isRequired: false,
paramIn: ParamIn.query,
def: 1,
),
ApiParameter<int>(
'limit',
isRequired: false,
paramIn: ParamIn.query,
def: 20,
),
],
response: {
'200': [
ApiResponse<int>('count', def: 0),
ApiResponse<List>('rows', def: []),
],
'404': ApiDocuments.r404,
},
),
),
),
ApiDoc 属性
ApiDoc 既可以用在路由级别,也可以按 HTTP 方法分别使用。为每个方法嵌套一个内部 ApiDoc——路由级别的 parameters/body/response 会应用于每一种方法,而 Finch 在构建规范时,会将按方法设置的那个(get/post/put/delete)合并叠加在其上:
ApiDoc(
get: ApiDoc(description: '...', parameters: [...], response: {...}),
post: ApiDoc(description: '...', parameters: [...], body: [...], response: {...}),
put: ApiDoc(description: '...', parameters: [...], body: [...], response: {...}),
delete: ApiDoc(description: '...', parameters: [...], response: {...}),
)
ApiParameter
ApiParameter<T> 描述一个不属于请求体的单一输入参数——路径片段、查询字符串、请求头或 Cookie:
| 属性 | 类型 | 描述 |
|---|---|---|
| 第一个参数 | String |
参数名称 |
isRequired |
bool |
该字段是否必填(默认为 false) |
paramIn |
ParamIn |
值的来源(默认为 ParamIn.query) |
def |
T? |
在 Swagger UI 中显示的默认值/示例值 |
description |
String? |
在 Swagger UI 中显示的可选描述 |
ParamIn 的取值——并不存在 ParamIn.body;请改用 ApiBodyField 来描述请求体字段(见下文):
| 值 | 描述 |
|---|---|
ParamIn.path |
URL 路径片段(例如:/books/{id}) |
ParamIn.query |
查询字符串(例如:?page=1) |
ParamIn.header |
HTTP 请求头 |
ParamIn.cookie |
请求 Cookie |
ApiBodyField —— 描述请求体
对于 POST/PUT 路由,应在 ApiDoc 的 body 列表中使用 ApiBodyField<T> 来描述 JSON 请求体——而不是使用 ApiParameter/ParamIn:
ApiDoc(
body: [
ApiBodyField<String>('title', isRequired: true, description: 'Book title'),
ApiBodyField<String>('author', isRequired: true),
],
response: {
'200': [ApiResponse<bool>('success', def: true)],
},
)
ApiResponse
ApiResponse<T> 描述响应体中的一个字段:
ApiResponse<String>('title', def: 'Book Title'),
ApiResponse<int>('id', def: 0),
ApiResponse<bool>('success', def: true),
ApiResponse<Map<String, dynamic>>('data', def: {}),
可复用的响应结构
Finch 并没有内置任何形如 r_401/r_404/r_500 的预定义常量——这种命名只是示例项目自己的约定,并不是框架本身的功能。你可以自行定义一次性的 List<ApiResponse> 常量,并在多个路由间复用,从而避免重复编写常见的错误响应结构:
class ApiDocuments {
static final r404 = [
ApiResponse<int>('timestamp_start', def: 0),
ApiResponse<String>('message', def: 'Not found'),
ApiResponse<bool>('success', def: false),
ApiResponse<int>('status', def: 404),
];
static final r403 = [
ApiResponse<int>('timestamp_start', def: 0),
ApiResponse<String>('message', def: 'Please login'),
ApiResponse<bool>('success', def: false),
ApiResponse<int>('status', def: 403),
];
}
// 在你的 response map 中使用:
response: {
'200': [...],
'403': ApiDocuments.r403,
'404': ApiDocuments.r404,
}
完整路由示例
FinchRoute(
key: 'api.books.one',
path: 'api/books/{id}',
methods: Methods.GET_POST,
index: booksController.one,
apiDoc: ApiDoc(
get: ApiDoc(
description: 'Get a single book by ID.',
parameters: [
ApiParameter<String>('id', isRequired: true, paramIn: ParamIn.path),
],
response: {
'200': [
ApiResponse<int>('id', def: 0),
ApiResponse<String>('title', def: ''),
ApiResponse<String>('author', def: ''),
],
'404': ApiDocuments.r404,
},
),
post: ApiDoc(
description: 'Update a book by ID.',
parameters: [
ApiParameter<String>('id', isRequired: true, paramIn: ParamIn.path),
],
body: [
ApiBodyField<String>('title', isRequired: false),
ApiBodyField<String>('author', isRequired: false),
],
response: {
'200': [ApiResponse<bool>('success', def: true)],
'404': ApiDocuments.r404,
},
),
),
),