API-documentatie (Swagger / OpenAPI)

Finch heeft een ingebouwd OpenAPI-documentatiesysteem. Je beschrijft elke API-route met ApiDoc, en Finch genereert een machineleesbare OpenAPI 3.1 JSON-spec en een Swagger UI waarmee ontwikkelaars je API vanuit de browser kunnen verkennen en testen.

Het opzetten van API-documentatie vereist drie stappen:

  1. Maak een ApiController-instantie aan.
  2. Registreer de documentatieroutes (OpenAPI JSON-output en Swagger UI).
  3. Koppel ApiDoc-objecten aan de routes die in de documentatie moeten verschijnen.

Stap 1 — Maak de ApiController aan

ApiController leest alle routes van je app en genereert de OpenAPI-spec:

final apiController = ApiController(
  title: 'My App API',
  app: app,
  security: 'apiKey', // optioneel — naam die wordt getoond voor het bearer-beveiligingsschema, standaard 'apiKey'
);

Stap 2 — Registreer de documentatieroutes

Voeg twee routes toe aan je router: één voor de ruwe OpenAPI JSON en één voor de Swagger UI:

[
  // Geeft de OpenAPI JSON-spec terug — gebruikt door Swagger UI en API-clients
  FinchRoute(
    key: 'root.api.docs',
    path: 'api/docs',
    index: apiController.indexPublic,
  ),

  // Swagger UI-pagina — geef de URL naar de JSON-spec door
  FinchRoute(
    key: 'root.swagger',
    path: 'swagger',
    index: () => apiController.swagger(rq.url('api/docs')),
  ),
];

Open /swagger in je browser om de interactieve documentatie te zien.

Standaard is de Swagger UI (en het gewone index() JSON-eindpunt) alleen toegankelijk in local debug-modus (isLocalDebug: true). indexPublic() is een snelkoppeling voor index(showPublic: true), dus de JSON-specroute hierboven is met opzet openbaar — om ook apiController.swagger(...) openbaar te maken, ongeacht de debug-modus, geef je er expliciet showPublic: true aan door.

Alleen routes waarvan het pad begint met /api worden opgenomen in de gegenereerde spec, en alleen als ze een apiDoc hebben gekoppeld.

Stap 3 — Definieer ApiDoc voor routes

Elke route kan een ApiDoc hebben die beschrijft wat de route doet, welke parameters/body hij accepteert en welke responses hij teruggeeft. Koppel het aan een FinchRoute via de apiDoc-eigenschap.

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-eigenschappen

ApiDoc kan op routeniveau of per HTTP-methode worden gebruikt. Nest een innerlijke ApiDoc voor elke methode — een parameters/body/response op routeniveau geldt voor elke methode, en die per methode (get/post/put/delete) wordt daar bovenop samengevoegd wanneer Finch de spec opbouwt:

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> beschrijft één invoerparameter die geen deel uitmaakt van de requestbody — padsegmenten, query-strings, headers of cookies:

Eigenschap Type Beschrijving
Eerste argument String Parameternaam
isRequired bool Of het veld verplicht is (standaard false)
paramIn ParamIn Waar de waarde vandaan komt (standaard ParamIn.query)
def T? Standaard-/voorbeeldwaarde die in Swagger UI wordt getoond
description String? Optionele beschrijving die in Swagger UI wordt getoond

ParamIn-waarden — er bestaat geen ParamIn.body; documenteer requestbody-velden in plaats daarvan met ApiBodyField (zie hieronder):

Waarde Beschrijving
ParamIn.path URL-padsegment (bijv. /books/{id})
ParamIn.query Query-string (bijv. ?page=1)
ParamIn.header HTTP-requestheader
ParamIn.cookie Requestcookie

ApiBodyField — De requestbody documenteren

Beschrijf voor POST/PUT-routes de JSON-requestbody met ApiBodyField<T> in de body-lijst van ApiDoc — niet met 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> beschrijft een veld in de responsebody:

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

Herbruikbare responsvormen

Finch levert geen vooraf gedefinieerde r_401/r_404/r_500-achtige constanten — die naamgeving is slechts een conventie uit het voorbeeldproject, geen framework-functie. Definieer je eigen List<ApiResponse>-constanten één keer en hergebruik ze over routes heen om veelvoorkomende foutvormen niet steeds opnieuw te hoeven schrijven:

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

Volledig routevoorbeeld

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