API 文档(Swagger / OpenAPI)

Finch 内置了 OpenAPI 文档系统。你使用 ApiDoc 描述每个 API 路由,Finch 会生成机器可读的 OpenAPI JSON 规范,以及一个 Swagger UI,让开发者可以在浏览器中浏览并测试你的 API。

设置 API 文档需要三个步骤:

  1. 创建一个 ApiController 实例。
  2. 注册文档路由(OpenAPI JSON 输出和 Swagger UI)。
  3. 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,
),