API Documentation (Swagger / OpenAPI)

Finch has a built-in OpenAPI documentation system. You describe each API route using ApiDoc, and Finch generates a machine-readable OpenAPI 3.1 JSON spec and a Swagger UI that lets developers explore and test your API from a browser.

Setting up API docs requires three steps:

  1. Create an ApiController instance.
  2. Register the documentation routes (OpenAPI JSON output and Swagger UI).
  3. Attach ApiDoc objects to the routes that should appear in the documentation.

Step 1 — Create the ApiController

ApiController reads all routes from your app and generates the OpenAPI spec:

final apiController = ApiController(
  title: 'My App API',
  app: app,
  security: 'apiKey', // optional — name shown for the bearer security scheme, default 'apiKey'
);

Step 2 — Register Documentation Routes

Add two routes to your router: one for the raw OpenAPI JSON and one for the Swagger UI:

[
  // Returns the OpenAPI JSON spec — used by Swagger UI and API clients
  FinchRoute(
    key: 'root.api.docs',
    path: 'api/docs',
    index: apiController.indexPublic,
  ),

  // Swagger UI page — pass the URL to the JSON spec
  FinchRoute(
    key: 'root.swagger',
    path: 'swagger',
    index: () => apiController.swagger(rq.url('api/docs')),
  ),
];

Open /swagger in your browser to see the interactive documentation.

By default the Swagger UI (and the plain index() JSON endpoint) is only accessible in local debug mode (isLocalDebug: true). indexPublic() is a shortcut for index(showPublic: true), so the JSON spec route above is public by design — to also make apiController.swagger(...) public regardless of debug mode, pass showPublic: true to it explicitly.

Only routes whose path starts with /api are included in the generated spec, and only if they have an apiDoc attached.

Step 3 — Define ApiDoc for Routes

Each route can have an ApiDoc that describes what the route does, what parameters/body it accepts, and what responses it returns. Attach it to a FinchRoute via the apiDoc property.

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 Properties

ApiDoc can be used at the route level or per HTTP method. Nest an inner ApiDoc for each method — a route-level parameters/body/response applies to every method, and the per-method one (get/post/put/delete) is merged on top of it when Finch builds the spec:

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> describes a single input parameter that is not part of the request body — path segments, query strings, headers, or cookies:

Property Type Description
First arg String Parameter name
isRequired bool Whether the field is required (default false)
paramIn ParamIn Where the value comes from (default ParamIn.query)
def T? Default/example value shown in Swagger UI
description String? Optional description shown in Swagger UI

ParamIn values — there is no ParamIn.body; document request body fields with ApiBodyField instead (see below):

Value Description
ParamIn.path URL path segment (e.g., /books/{id})
ParamIn.query Query string (e.g., ?page=1)
ParamIn.header HTTP request header
ParamIn.cookie Request cookie

ApiBodyField — Documenting the Request Body

For POST/PUT routes, describe the JSON request body with ApiBodyField<T> in ApiDoc's body list — not with 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> describes a field in the response body:

ApiResponse<String>('title', def: 'Book Title'),
ApiResponse<int>('id', def: 0),
ApiResponse<bool>('success', def: true),
ApiResponse<Map<String, dynamic>>('data', def: {}),

Reusable Response Shapes

Finch does not ship any predefined r_401/r_404/r_500-style constants — that naming is just a convention from the example project, not a framework feature. Define your own List<ApiResponse> constants once and reuse them across routes to avoid repeating common error shapes:

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),
  ];
}
// Use in your response map:
response: {
  '200': [...],
  '403': ApiDocuments.r403,
  '404': ApiDocuments.r404,
}

Complete Route Example

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,
      },
    ),
  ),
),