Swagger And API Docs
Flint API docs are generated from route source comments. The route file is the source of truth; docs/swagger.json is generated output.
Use this guide when adding, reviewing, or fixing API documentation in a Flint app. Anyone should be able to read a route group, document the route comments, run the generator, and know what Swagger UI will show.
Before documenting an app, inspect:
lib/main.dartforFlint(enableSwaggerDocs: true), registered route groups, and mounted modules.lib/routes/for theRouteGroupclasses the generator will parse.lib/controllers/for the real request behavior.lib/middlewares/for auth, role, tenant, or rate-limit middleware that must be reflected with@author response codes.lib/models/and resource/presenter classes for response shapes.- Authentication before documenting auth, password reset, send OTP, verify OTP, resend OTP, refresh token, or OAuth routes.
- Routing before documenting route params,
QUERY, WebSockets, route groups, or controller routes. - Validation before documenting request bodies and validation failures.
Framework source to inspect when behavior is unclear:
lib/src/cli/generatedocscommand.dartlib/src/swaggergen/routeparser.dartlib/src/swaggergen/routeextractor.dartlib/src/swaggergen/docparser.dartlib/src/swaggergen/swaggergenerator.dartlib/src/app.dart
The Workflow
- Put routes in
lib/routes/<feature>_routes.dart. - Use one
RouteGroupclass per route file. - Set
String get prefix => '/feature';. - Set
String get tag => 'Feature';. - Put
///docs directly above each route call. - Run
dart run flint_dart:flint --docs-generate. - Commit route source comments and generated
docs/swagger.jsonwhen the app wants generated docs tracked. - Serve docs with
Flint(enableSwaggerDocs: true).
Do not hand-edit docs/swagger.json as the primary fix. Update route comments, then regenerate.
Serving Swagger UI
Enable docs routes in the app:
final app = Flint(enableSwaggerDocs: true);
This registers:
GET /swagger.json, servingdocs/swagger.jsonfirst, thenswagger.json.GET /docs, serving bundled Swagger UI when assets can be found.GET /swagger-ui/*, serving static Swagger UI assets.
Swagger UI lookup checks:
FLINTSWAGGERUI_DIR- project-local
swagger-ui - project-local
build/swagger-ui - project-local
lib/swagger/swagger-ui - framework/package Swagger UI asset locations
Runtime /swagger.json does not generate docs. It only serves an existing Swagger file. Run --docs-generate first.
Generating docs/swagger.json
Run:
dart run flint_dart:flint --docs-generate
GenerateDocsCommand reads every .dart file under lib/routes recursively. It parses route groups, route paths, route comments, request bodies, parameters, responses, auth markers, servers, WebSocket routes, and Flint QUERY routes. It then writes:
docs/swagger.json
The generated object uses OpenAPI 3.0.0, the default title Flint API, and the default version 1.0.0.
What The Generator Parses
The generator parses route calls inside classes that extend RouteGroup.
Recognized route calls:
app.get('/path', handler);
app.post('/path', handler);
app.put('/path', handler);
app.patch('/path', handler);
app.delete('/path', handler);
app.query('/path', handler);
app.websocket('/path', handler);
routes.get('/path', (controller) => controller.index());
routes.post('/path', (controller) => controller.store());
The parser also recognizes route calls split across chains when the route variable and method are easy to see:
routes
.post('/', (controller) => controller.store())
.useMiddleware(AuthMiddleware());
Prefer keeping the route method visible as routes.post(...), courses.get(...), or app.websocket(...). Do not hide route registration behind helper methods if you expect --docs-generate to find it.
Current parser limits:
- It only parses files under
lib/routes. - It only records routes found inside
RouteGroupclasses. - It does not inspect controller methods.
- It does not infer auth from middleware; use
@auth. - It does not infer request bodies from
req.validate(...); use@body. - It does not infer query parameters from
req.queryParam(...); use@query. - It does not parse arbitrary
app.route('METHOD', ...)calls yet. - It creates JSON request bodies only; multipart upload schemas need generator work before they can be represented precisely.
RouteGroup Prefix And Tag
Use the RouteGroup getters for normal docs:
class CourseRoutes extends RouteGroup {
@override
String get prefix => '/courses';
@override
String get tag => 'Courses';
@override
void register(Flint app) {
final courses = app.controller(CourseController.new);
/// @summary List courses
/// @response 200 Courses loaded
courses.get('/', (controller) => controller.index());
}
}
Swagger will see:
{
"paths": {
"/courses": {
"get": {
"summary": "List courses",
"tags": ["Courses"],
"responses": {
"200": {"description": "Courses loaded"}
}
}
}
}
}
If no tag getter is found, the operation tag becomes Default.
@prefix also exists, but use it carefully. It overrides the prefix used by the docs generator. If @prefix does not match the real RouteGroup.prefix, Swagger will show a different path from the runtime app.
Prefer this:
String get prefix => '/courses';
Avoid this unless you intentionally need a docs-only override:
/// @prefix /api/courses
class CourseRoutes extends RouteGroup {
@override
String get prefix => '/courses';
}
Route Comment Placement
Put route comments immediately above the route call they describe.
/// @summary Create course
/// @response 201 Course created
/// @response 422 Validation failed
/// @body {"title": "string", "status": "string"}
courses.post('/', (controller) => controller.store());
Do not put route docs only above the controller method. The generator reads lib/routes, not controller action bodies.
Good:
/// @summary Show course
/// @param id path string required Course ID
/// @response 200 Course loaded
/// @response 404 Course not found
courses.get('/:id', (controller) => controller.show());
Not enough for Swagger generation:
class CourseController extends Controller {
/// @summary Show course
Future<Response> show() async {
return res.json({'data': await Course().find(req.param('id'))});
}
}
Supported Annotations
@summary
Sets the operation summary.
/// @summary Register a new user
auth.post('/register', (controller) => controller.register());
Swagger will see:
{"summary": "Register a new user"}
Keep summaries short and action-based: List courses, Create course, Verify email OTP, Refresh access token.
@response
Adds a response status code and description.
/// @response 200 User loaded
/// @response 401 Unauthorized
/// @response 404 User not found
users.get('/:id', (controller) => controller.show());
The first token after @response is the status code. Everything after it is the description.
Swagger will see:
{
"responses": {
"200": {"description": "User loaded"},
"401": {"description": "Unauthorized"},
"404": {"description": "User not found"}
}
}
If a route has no @response, Flint generates:
{"200": {"description": "OK"}}
Common response codes:
200for successful reads and updates.201for created resources.202for accepted background work.204for successful empty responses.400for malformed input.401for unauthenticated requests.403for authenticated users without permission.404for missing resources.409for conflicts.422for validation failures.429for rate limits.500for unexpected server errors.
@param
Documents a parameter in path, query, header, or another OpenAPI parameter location.
Format:
@param <name> <location> <type> <required|optional> <description>
Example:
/// @summary Show course
/// @param id path string required Course ID
/// @response 200 Course loaded
/// @response 404 Course not found
courses.get('/:id', (controller) => controller.show());
Flint automatically converts :id in the route path to {id} and creates a string path parameter. Use @param when you want a better type or description.
Swagger will see:
{
"paths": {
"/courses/{id}": {
"get": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {"type": "string"},
"description": "Course ID"
}
]
}
}
}
}
Path parameters are always marked required, even if the annotation says optional, because OpenAPI requires path params to be required.
@query
Documents a query-string parameter.
Format:
@query <name> <type> <required|optional> <description>
Example:
/// @summary List courses
/// @query page integer optional Page number
/// @query perPage integer optional Items per page
/// @query status string optional Filter by course status
/// @response 200 Courses loaded
courses.get('/', (controller) => controller.index());
Swagger will see query parameters on the operation:
{
"parameters": [
{
"name": "page",
"in": "query",
"schema": {"type": "integer"},
"required": false,
"description": "Page number"
}
]
}
Use OpenAPI type names such as string, integer, number, boolean, array, and object.
@body
Documents a JSON request body.
Example:
/// @summary Create course
/// @response 201 Course created
/// @response 422 Validation failed
/// @body {"title": "string", "price": "number", "published": "boolean"}
courses.post('/', (controller) => controller.store());
Swagger will see:
{
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"price": {"type": "number"},
"published": {"type": "boolean"}
}
}
}
}
}
}
@body accepts balanced JSON across multiple doc lines:
/// @body {
/// "email": "string",
/// "password": "string",
/// "profile": {
/// "firstName": "string",
/// "lastName": "string"
/// },
/// "roles": "string[]"
/// }
auth.post('/register', (controller) => controller.register());
Supported type hints inside @body:
"string""integer""number""boolean""object""array""string[]","integer[]","number[]", or"boolean[]"- example values such as
1,1.5,true,null, nested objects, and arrays
Current @body generation does not mark individual properties as required. Document validation failures with @response 422 Validation failed and keep the real validation rules in the controller or validator class.
@auth
Adds OpenAPI security to the operation.
/// @summary Current user
/// @auth bearer
/// @response 200 Current user loaded
/// @response 401 Unauthorized
auth.get('/me', (controller) => controller.me())
.useMiddleware(AuthMiddleware());
Swagger will see:
{
"security": [
{"bearer": []}
]
}
If @auth has no value, Flint uses bearer.
/// @auth
Built-in security schemes in generated Swagger:
{
"bearer": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"},
"basicAuth": {"type": "http", "scheme": "basic"}
}
Use @auth bearer for JWT and token-protected routes. Use @auth basicAuth only when the route actually uses HTTP Basic auth. If a route has auth middleware but no @auth, Swagger will look public.
@server
Adds a server URL to the generated top-level servers list.
/// @summary List public courses
/// @server https://api.example.com
/// @server http://localhost:3000
/// @response 200 Courses loaded
courses.get('/', (controller) => controller.index());
Swagger will see:
{
"servers": [
{"url": "https://api.example.com"},
{"url": "http://localhost:3000"}
]
}
Servers are collected globally and deduplicated. They are not stored only on that one route.
@prefix
Overrides the route-group prefix for generated docs.
/// @prefix /api/courses
Use the RouteGroup.prefix getter for normal app code. Use @prefix only when the source parser cannot see the actual prefix or when you intentionally need a docs-only prefix. A wrong @prefix makes Swagger paths differ from runtime paths.
Complete CRUD Example
File: lib/routes/course_routes.dart
import 'package:flint_dart/flint_dart.dart';
import '../controllers/course_controller.dart';
import '../middlewares/auth_middleware.dart';
class CourseRoutes extends RouteGroup {
@override
String get prefix => '/courses';
@override
String get tag => 'Courses';
@override
void register(Flint app) {
final courses = app.controller(CourseController.new);
/// @summary List courses
/// @query page integer optional Page number
/// @query perPage integer optional Items per page
/// @query status string optional Filter by status
/// @response 200 Courses loaded
courses.get('/', (controller) => controller.index());
/// @summary Create course
/// @auth bearer
/// @response 201 Course created
/// @response 401 Unauthorized
/// @response 422 Validation failed
/// @body {"title": "string", "status": "string"}
courses.post('/', (controller) => controller.store())
.useMiddleware(AuthMiddleware());
/// @summary Show course
/// @param id path string required Course ID
/// @response 200 Course loaded
/// @response 404 Course not found
courses.get('/:id', (controller) => controller.show());
/// @summary Update course
/// @auth bearer
/// @param id path string required Course ID
/// @response 200 Course updated
/// @response 401 Unauthorized
/// @response 404 Course not found
/// @response 422 Validation failed
/// @body {"title": "string", "status": "string"}
courses.patch('/:id', (controller) => controller.update())
.useMiddleware(AuthMiddleware());
/// @summary Delete course
/// @auth bearer
/// @param id path string required Course ID
/// @response 204 Course deleted
/// @response 401 Unauthorized
/// @response 404 Course not found
courses.delete('/:id', (controller) => controller.destroy())
.useMiddleware(AuthMiddleware());
}
}
Swagger will see:
/courseswithgetandpost./courses/{id}withget,patch, anddelete.- tag
Courseson every operation. - bearer security only on routes that have
@auth bearer. - path parameter
idon ID routes. - query parameters on the list route.
- JSON request bodies on create and update routes.
Auth And OTP Routes
Auth routes should be documented carefully because clients need to know which routes are public, which route sends an OTP, and which route verifies one.
Example:
class AuthRoutes extends RouteGroup {
@override
String get prefix => '/auth';
@override
String get tag => 'Auth';
@override
void register(Flint app) {
final auth = app.controller(AuthController.new);
/// @summary Register account
/// @response 201 Account registered
/// @response 409 Email already exists
/// @response 422 Validation failed
/// @body {"email": "string", "password": "string", "name": "string"}
auth.post('/register', (controller) => controller.register());
/// @summary Send email verification OTP
/// @response 200 OTP sent
/// @response 404 Account not found
/// @response 422 Validation failed
/// @body {"email": "string"}
auth.post('/send-otp', (controller) => controller.sendOtp());
/// @summary Verify email OTP
/// @response 200 Email verified
/// @response 400 Invalid or expired OTP
/// @response 422 Validation failed
/// @body {"email": "string", "otp": "string"}
auth.post('/verify-otp', (controller) => controller.verifyOtp());
/// @summary Resend email verification OTP
/// @response 200 OTP resent
/// @response 404 Account not found
/// @response 429 Too many requests
/// @response 422 Validation failed
/// @body {"email": "string"}
auth.post('/resend-otp', (controller) => controller.resendOtp());
/// @summary Current user
/// @auth bearer
/// @response 200 Current user loaded
/// @response 401 Unauthorized
auth.get('/me', (controller) => controller.me())
.useMiddleware(AuthMiddleware());
}
}
Do not mark public login, register, send OTP, verify OTP, or forgot-password routes with @auth bearer unless they truly require an existing token. Do mark current-user, logout, token refresh, and protected account routes when they need auth.
QUERY Routes
Flint supports HTTP QUERY for safe, idempotent reads with a request body. OpenAPI 3.0 has no standard query operation key, so Flint stores it as a vendor extension.
Route:
/// @summary Search courses
/// @query page integer optional Page number
/// @response 200 Search results loaded
/// @body {"q": "string", "status": "string"}
courses.query('/search', (controller) => controller.search());
Swagger will see:
{
"paths": {
"/courses/search": {
"x-flint-query": {
"x-http-method": "QUERY",
"x-openapi-operation-key-unavailable": true,
"summary": "Search courses"
}
}
},
"x-flint-query-routes": {
"/courses/search": {
"x-http-method": "QUERY"
}
},
"x-flint-query-openapi-note": "OpenAPI 3.0 has no standard QUERY operation key. Flint preserves HTTP QUERY operations with x-http-method: QUERY."
}
Swagger UI may not display QUERY like standard GET or POST operations. Clients should read the Flint extension fields.
WebSocket Routes
Document the WebSocket handshake route where the client connects.
Route:
class ChatRoutes extends RouteGroup {
@override
String get prefix => '/ws';
@override
String get tag => 'Chat';
@override
void register(Flint app) {
/// @summary Chat websocket handshake
/// @param room path string required Chat room
/// @response 101 Switching Protocols
app.websocket('/chat/:room', (Context ctx) {
ctx.socket?.emit('ready', {'ok': true});
});
}
}
Swagger will see a GET operation with WebSocket extensions:
{
"paths": {
"/ws/chat/{room}": {
"get": {
"summary": "Chat websocket handshake",
"x-websocket": true,
"x-flint-transport": "websocket",
"x-flint-namespace": "/ws/chat/{room}",
"responses": {
"101": {"description": "Switching Protocols"}
}
}
}
},
"x-websockets": {
"/ws/chat/{room}": {
"x-websocket": true,
"x-flint-transport": "websocket"
}
}
}
Document event names, payloads, and room behavior in WebSockets or a feature-specific markdown file. Swagger describes only the connection endpoint.
File Upload Routes
The current generator creates application/json request bodies from @body. It does not yet create precise multipart/form-data schemas.
For upload routes, document what the route does, auth, status codes, and the file field name in the summary or body description until multipart support is added.
Example:
/// @summary Upload profile avatar using multipart field "avatar"
/// @auth bearer
/// @response 200 Avatar uploaded
/// @response 401 Unauthorized
/// @response 422 Avatar file is required or invalid
profile.post('/avatar', (controller) => controller.uploadAvatar())
.useMiddleware(AuthMiddleware());
What To Document On Every Route
For each route, include:
@summarywith a short action phrase.@responsefor success.@responsefor validation, auth, not-found, conflict, and rate-limit outcomes that can happen.@paramfor every meaningful path parameter.@queryfor every supported query-string parameter.@bodyfor JSON payloads on create, update, login, OTP, and search routes.@auth beareror@auth basicAuthwhen middleware or controller behavior requires auth.
A protected mutation should usually have:
/// @summary Publish course
/// @auth bearer
/// @param id path string required Course ID
/// @response 200 Course published
/// @response 401 Unauthorized
/// @response 403 Forbidden
/// @response 404 Course not found
/// @response 422 Validation failed
courses.post('/:id/publish', (controller) => controller.publish())
.useMiddleware(AuthMiddleware());
Good Documentation Style
Prefer specific language:
Create courseVerify email OTPRefresh access tokenUpload profile avatarList published courses
Avoid vague language:
Get dataDo requestUser APISuccess response descriptionCreate item by idfor a route that does not use an ID
Keep route comments close to route behavior. If the controller changes from 200 to 201, update the route comments before regenerating docs.
Generated Output Shape
The generated Swagger file has this shape:
{
"openapi": "3.0.0",
"info": {
"title": "Flint API",
"version": "1.0.0"
},
"servers": [],
"paths": {},
"x-websockets": {},
"x-flint-query-routes": {},
"components": {
"securitySchemes": {
"bearer": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
},
"basicAuth": {
"type": "http",
"scheme": "basic"
}
}
}
}
Empty extension fields are omitted. servers is omitted when no @server annotation is present.
Common Mistakes
- Do not document only controller methods; document the route calls in
lib/routes. - Do not hand-edit
docs/swagger.jsoninstead of fixing route comments. - Do not use
@prefixunless it matches runtime routing or you intentionally need a docs-only override. - Do not forget
@authon protected routes; middleware is not inferred. - Do not put fake
401responses on public routes unless the route can actually return401. - Do not add
@bodyto every route; use it for routes that read JSON bodies. - Do not rely on the generator to infer
req.validate(...); write@body,@query, and@param. - Do not split route registration through custom helper methods if the generator needs to see it.
- Do not expect
app.route('OPTIONS', ...)to be parsed until route-extractor support is added. - Do not treat
QUERYas a standard OpenAPI method; Flint stores it in extension fields. - Do not expect Swagger to describe WebSocket event payloads; document those in WebSockets.
Review Checklist
Before running --docs-generate, check:
- Every public API route lives in
lib/routes. - Every route group has a real
prefixand usefultag. - Every route has a specific
@summary. - Every route has realistic success and error
@responseentries. - Every
:idor path variable has a matching@paramwhen type or description matters. - Every query filter, pagination value, or search option has
@query. - Every JSON body has
@body. - Every protected route has
@auth. - Auth and OTP routes were checked against Authentication.
- WebSocket routes were checked against WebSockets.
docs/swagger.jsonwas regenerated after route comments changed.
One language powering Full-Stack Web, Cross-Platform Clients, Native AI, and Connected Robotics.