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:
- Create an
ApiControllerinstance. - Register the documentation routes (OpenAPI JSON output and Swagger UI).
- Attach
ApiDocobjects 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 forindex(showPublic: true), so the JSON spec route above is public by design — to also makeapiController.swagger(...)public regardless of debug mode, passshowPublic: trueto 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,
},
),
),
),