API 文档(Swagger / OpenAPI)
Finch 内置了 OpenAPI 文档系统。你使用 ApiDoc 描述每个 API 路由,Finch 会生成机器可读的 OpenAPI JSON 规范,以及一个 Swagger UI,让开发者可以在浏览器中浏览并测试你的 API。
设置 API 文档需要三个步骤:
- 创建一个
ApiController实例。 - 注册文档路由(OpenAPI JSON 输出和 Swagger UI)。
- 将
ApiDoc对象附加到应在文档中显示的路由上。
第 1 步 — 创建 ApiController
ApiController 会读取应用中的所有路由并生成 OpenAPI 规范:
final apiController = ApiController(
title: 'My App API',
app: app,
);
第 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 仅在本地调试模式(
isLocalDebug: true)下可访问。若要公开访问,请在apiController.swagger(..., showPublic: true)中传入showPublic: true。
第 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: []),
],
'401': r_401,
'404': r_404,
},
),
),
),
ApiDoc 属性
ApiDoc 可以用在路由级别,也可以按 HTTP 方法使用。为每个方法嵌套一个内部 ApiDoc:
ApiDoc(
get: ApiDoc(description: '...', parameters: [...], response: {...}),
post: ApiDoc(description: '...', parameters: [...], response: {...}),
put: ApiDoc(description: '...', parameters: [...], response: {...}),
delete: ApiDoc(description: '...', parameters: [...], response: {...}),
)
ApiParameter
ApiParameter<T> 描述单个输入参数:
| 属性 | 类型 | 描述 |
|---|---|---|
| 第一个参数 | String |
参数名称 |
isRequired |
bool |
该字段是否必填 |
paramIn |
ParamIn |
值的来源 |
def |
T? |
默认值示例 |
ParamIn 的取值:
| 值 | 描述 |
|---|---|
ParamIn.path |
URL 路径片段(例如:/books/{id}) |
ParamIn.query |
查询字符串(例如:?page=1) |
ParamIn.header |
HTTP 请求头 |
ParamIn.body |
请求体(POST/PUT) |
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 为常见的 HTTP 错误码提供了现成的响应列表:
// 在你的 response map 中使用:
response: {
'200': [...],
'401': r_401, // Unauthorized
'404': r_404, // Not found
'500': r_500, // Server error
}
完整路由示例
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': r_404,
},
),
post: ApiDoc(
description: 'Update a book by ID.',
parameters: [
ApiParameter<String>('id', isRequired: true, paramIn: ParamIn.path),
ApiParameter<String>('title', isRequired: false, paramIn: ParamIn.body),
ApiParameter<String>('author', isRequired: false, paramIn: ParamIn.body),
],
response: {
'200': [ApiResponse<bool>('success', def: true)],
'404': r_404,
},
),
),
),
API 文档控制器
final apiController = ApiController(
title: "API Documentation",
app: app,
);
// 应添加到你的 FinchApp 路由中的路由
// OpenApi json 输出
FinchRoute(
key: 'root.api.docs',
path: 'api/docs',
index: apiController.indexPublic,
),
// Swagger UI
FinchRoute(
key: 'root.swagger',
path: 'swagger',
index: () => apiController.swagger(rq.url('api/docs')),
),
为每个路由定义 ApiDoc
class ApiDocuments {
static Future<ApiDoc> onePerson() async {
return ApiDoc(
post: ApiDoc(
response: {
'200': [
ApiResponse<int>('timestamp_start', def: 0),
ApiResponse<bool>('success', def: true),
ApiResponse<Map<String, String>>(
'data',
def: PersonCollectionFree.formPerson.fields.map((k, v) {
return MapEntry(k, v.defaultValue?.call());
}),
),
],
'404': r_404,
},
description: "Update one person by id.",
parameters: [
ApiParameter<String>(
'id',
isRequired: true,
paramIn: ParamIn.path,
),
ApiParameter<String>(
'name',
isRequired: false,
paramIn: ParamIn.header,
),
ApiParameter<int>(
'age',
isRequired: false,
paramIn: ParamIn.header,
),
ApiParameter<double>(
'height',
isRequired: false,
paramIn: ParamIn.header,
),
ApiParameter<String>(
'email',
isRequired: true,
paramIn: ParamIn.header,
),
ApiParameter<String>(
'married',
isRequired: false,
paramIn: ParamIn.header,
def: false,
),
],
),
get: ApiDoc(
response: {
'200': [
ApiResponse<int>('timestamp_start', def: 0),
ApiResponse<Map<String, String>>(
'data',
def: PersonCollectionFree.formPerson.fields.map((k, v) {
return MapEntry(k, v.defaultValue?.call());
}),
),
],
'404': r_404,
},
description: "Get one person by id.",
parameters: [
ApiParameter<String>('id', isRequired: true, paramIn: ParamIn.path),
],
),
delete: ApiDoc(
response: {
'200': [
ApiResponse<int>('timestamp_start', def: 0),
ApiResponse<bool>('success', def: true),
],
'404': r_404,
},
description: "Delete one person by id.",
parameters: [
ApiParameter<String>('id', isRequired: true, paramIn: ParamIn.path),
],
),
);
}
}
/// 向路由添加 ApiDoc 的示例
FinchRoute(
key: 'root.person.show',
path: 'api/person/{id}',
extraPath: ['example/person/{id}'],
index: homeController.onePerson,
methods: Methods.GET_POST,
apiDoc: ApiDocuments.onePerson,
),