API-documentatie (Swagger / OpenAPI)

Finch heeft een ingebouwd OpenAPI-documentatiesysteem. Je beschrijft elke API-route met ApiDoc, en Finch genereert een machineleesbare OpenAPI 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,
);

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 alleen toegankelijk in local debug-modus (isLocalDebug: true). Om het openbaar te maken, geef je showPublic: true door aan apiController.swagger(..., showPublic: true).

Stap 3 — Definieer ApiDoc voor routes

Elke route kan een ApiDoc hebben die beschrijft wat de route doet, welke parameters 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: []),
        ],
        '401': r_401,
        '404': r_404,
      },
    ),
  ),
),

ApiDoc-eigenschappen

ApiDoc kan op routeniveau of per HTTP-methode worden gebruikt. Nest een innerlijke ApiDoc voor elke methode:

ApiDoc(
  get:    ApiDoc(description: '...', parameters: [...], response: {...}),
  post:   ApiDoc(description: '...', parameters: [...], response: {...}),
  put:    ApiDoc(description: '...', parameters: [...], response: {...}),
  delete: ApiDoc(description: '...', parameters: [...], response: {...}),
)

ApiParameter

ApiParameter<T> beschrijft een enkele invoerparameter:

Eigenschap Type Beschrijving
Eerste argument String Parameternaam
isRequired bool Of het veld verplicht is
paramIn ParamIn Waar de waarde vandaan komt
def T? Voorbeeld van standaardwaarde

ParamIn-waarden:

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

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

Voorgedefinieerde response-snelkoppelingen

Finch biedt kant-en-klare responselijsten voor veelvoorkomende HTTP-foutcodes:

// Gebruik in je response-map:
response: {
  '200': [...],
  '401': r_401,  // Unauthorized
  '404': r_404,  // Not found
  '500': r_500,  // Server error
}

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': 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-documentatiecontroller

final apiController = ApiController(
   title: "API Documentation",
   app: app,
);
// Routes die aan de routing van je FinchApp moeten worden toegevoegd
// OpenApi json-output
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 definiëren voor elke route

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

/// Voorbeeld van het toevoegen van ApiDoc aan routes
FinchRoute(
  key: 'root.person.show',
  path: 'api/person/{id}',
  extraPath: ['example/person/{id}'],
  index: homeController.onePerson,
  methods: Methods.GET_POST,
  apiDoc: ApiDocuments.onePerson,
),