Volund: Eyes of the Lord — Sees your code. Mirrors it into OpenAPI. No build_runner needed.
Like Adam from Record of Ragnarok with his Eyes of the Lord - this package uses dart:mirrors to instantly reflect your code structure. No build step. No configuration. It just sees and mirrors.
Unlike Flutter, Dart Frog runs on the server where dart:mirrors is available. This means:
- ✅ Auto-discover routes at runtime
- ✅ No build_runner configuration
- ✅ No code generation step
- ✅ No manual route imports
- ✅ Immediate feedback during development
dependencies:
dart_frog: ^1.0.0
dart_frog_openapi: ^0.1.0Annotations go where they belong - on the actual methods:
// routes/users/index.dart
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_openapi/dart_frog_openapi.dart';
@Api(tag: 'Users', description: 'User management')
Future<Response> onRequest(RequestContext context) async {
return switch (context.request.method) {
HttpMethod.get => _get(context),
HttpMethod.post => _post(context),
_ => Response(statusCode: 405),
};
}
@Get(summary: 'List users')
@QueryParam('page', type: 'integer', example: 1)
@BearerAuth()
@OkResponse(schema: User, isArray: true)
Future<Response> _get(RequestContext context) async {
// ...
}
@Post(summary: 'Create user')
@CreatedResponse(schema: User)
Future<Response> _post(
RequestContext context,
@Body(schema: CreateUserRequest) _, // Body on parameter
) async {
final body = await context.request.json();
// ...
}// routes/users/[id].dart
@Api(tag: 'Users')
Future<Response> onRequest(
RequestContext context,
@PathParam(description: 'User UUID', format: 'uuid') String id, // PathParam on parameter
) async {
return switch (context.request.method) {
HttpMethod.get => _get(context, id),
HttpMethod.put => _put(context, id),
HttpMethod.delete => _delete(context, id),
_ => Response(statusCode: 405),
};
}
@Get(summary: 'Get user')
@OkResponse(schema: User)
@NotFoundResponse(schema: ErrorResponse)
Future<Response> _get(RequestContext context, String id) async {
// ...
}
@Put(summary: 'Update user')
@OkResponse(schema: User)
Future<Response> _put(
RequestContext context,
String id,
@Body(schema: UpdateUserRequest) _,
) async {
final body = await context.request.json();
// ...
}
@Delete(summary: 'Delete user')
@NoContentResponse()
Future<Response> _delete(RequestContext context, String id) async {
// ...
}// routes/_middleware.dart
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_openapi/dart_frog_openapi.dart';
Handler middleware(Handler handler) {
return handler.use(
autoSwagger(title: 'My API', version: '1.0.0'),
);
}That's it. Routes are auto-discovered, methods are detected, docs are served.
- Swagger UI: http://localhost:8080/docs
- OpenAPI JSON: http://localhost:8080/openapi.json
- OpenAPI YAML: http://localhost:8080/openapi.yaml
// lib/models/user.dart
import 'package:dart_frog_openapi/dart_frog_openapi.dart';
@ApiSchema(description: 'User account')
class User {
@UuidProperty(description: 'Unique identifier')
final String id;
@EmailProperty()
final String email;
@ApiProperty(description: 'Display name', minLength: 2)
final String name;
const User({required this.id, required this.email, required this.name});
}| Annotation | Where it goes |
|---|---|
@Api |
On onRequest |
@Get, @Post, etc. |
On _get, _post, etc. helper methods |
@PathParam |
On the parameter in onRequest signature |
@QueryParam |
On the method that uses it |
@Body |
On a parameter in the method signature |
@OkResponse, etc. |
On the method |
The package auto-discovers HTTP methods by:
- Scanning all functions in the route file
- Reading
@Get,@Post,@Put,@Delete,@Patchannotations - Falling back to annotations on
onRequest
Function names don't matter - only the annotation:
@Get(summary: 'List users')
Future<Response> _listAllUsers(...) async { } // Works
@Get(summary: 'List users')
Future<Response> _fetchUsers(...) async { } // Works
@Get(summary: 'List users')
Future<Response> _xyz(...) async { } // Also worksGiven this file structure:
routes/
├── index.dart → /
├── users/
│ ├── index.dart → /users
│ └── [id].dart → /users/{id}
└── polls/
├── index.dart → /polls
└── [id]/
├── index.dart → /polls/{id}
└── choices/
├── index.dart → /polls/{id}/choices
└── [id].dart → /polls/{id}/choices/{id}
discoverRoutes() automatically:
- Scans all loaded route libraries via
dart:mirrors - Finds
onRequestfunctions - Converts file paths to OpenAPI paths (
[id]→{id}) - Returns a map ready for
addRoutes()
By default, [id] becomes {id}. For nested IDs, use discoverRoutesWithParams:
final apiSpec = OpenApiGenerator(title: 'My API')
.addRoutes(discoverRoutesWithParams({
'polls/[id]': 'pollId',
'polls/[id]/choices/[id]': 'choiceId',
}))
.generate();Results in /polls/{pollId}/choices/{choiceId}.
| Annotation | Description |
|---|---|
@Api(tag: 'Name') |
Groups endpoints under a tag |
@Get(summary: '...') |
GET operation |
@Post(summary: '...') |
POST operation |
@Put(summary: '...') |
PUT operation |
@Delete(summary: '...') |
DELETE operation |
@Patch(summary: '...') |
PATCH operation |
| Annotation | Description |
|---|---|
@PathParam('id') |
Path parameter |
@QueryParam('page') |
Query parameter |
@HeaderParam('X-Token') |
Header parameter |
@Body(schema: Type) |
Request body |
| Annotation | Description |
|---|---|
@ApiResponse(200, ...) |
Custom response |
@OkResponse(...) |
200 OK |
@CreatedResponse(...) |
201 Created |
@NoContentResponse() |
204 No Content |
@BadRequestResponse(...) |
400 Bad Request |
@UnauthorizedResponse(...) |
401 Unauthorized |
@NotFoundResponse(...) |
404 Not Found |
| Annotation | Description |
|---|---|
@BearerAuth() |
JWT bearer token |
@ApiKeyAuth() |
API key |
@BasicAuth() |
Basic auth |
@Public() |
No auth required |
| Annotation | Description |
|---|---|
@ApiSchema() |
Marks class as schema |
@ApiProperty() |
Documents property |
@UuidProperty() |
UUID format |
@EmailProperty() |
Email format |
@DateTimeProperty() |
DateTime format |
@PasswordProperty() |
Password (write-only) |
autoSwagger(
title: 'My API', // Required
version: '1.0.0', // Default: '1.0.0'
description: 'API desc', // Optional
docsPath: '/docs', // Default: '/docs'
specPath: '/openapi.json', // Default: '/openapi.json'
bearerAuth: true, // Default: true
apiKeyAuth: false, // Default: false
apiKeyHeader: 'X-API-Key', // Default: 'X-API-Key'
servers: [ // Optional
{'url': 'https://api.example.com', 'description': 'Production'},
],
paramNames: { // Optional - custom path param names
'polls/[id]': 'pollId',
'polls/[id]/choices/[id]': 'choiceId',
},
)For more control, use OpenApiGenerator directly:
// lib/api_spec.dart
import 'package:dart_frog_openapi/dart_frog_openapi.dart';
final apiSpec = OpenApiGenerator(
title: 'My API',
version: '1.0.0',
description: 'API description',
servers: [
{'url': 'http://localhost:8080', 'description': 'Dev'},
],
contact: {'name': 'Support', 'email': 'support@example.com'},
license: {'name': 'MIT'},
)
// Security schemes
.addBearerAuth(format: 'JWT')
.addApiKeyAuth(headerName: 'X-API-Key')
.addBasicAuth()
// Tags (optional - auto-derived from @Api)
.addTag('Users', description: 'User endpoints')
// Routes - auto-discover or manual
.addRoutes(discoverRoutes()) // Auto-discover all
// or
.addRoute('/users', usersHandler) // Manual
// Schemas (optional - auto-discovered from @Body/@Response)
.addSchema<User>()
.addSchema<CreateUserRequest>()
// Generate OpenAPI spec
.generate();
// routes/_middleware.dart
Handler middleware(Handler handler) {
return handler.use(swaggerUI(spec: apiSpec));
}handler.use(reDoc(spec: apiSpec, title: 'My API'))handler.use(scalar(spec: apiSpec, title: 'My API'))Handler middleware(Handler handler) {
final spec = OpenApiGenerator(
title: 'My API',
version: '1.0.0',
)
.addRoutes(discoverRoutes())
.generate();
return handler
.use(swaggerUI(spec: spec, docsPath: '/docs'))
.use(reDoc(spec: spec, docsPath: '/redoc'))
.use(scalar(spec: spec, docsPath: '/scalar'));
}Then access:
- http://localhost:8080/docs - Swagger UI
- http://localhost:8080/redoc - ReDoc
- http://localhost:8080/scalar - Scalar
- http://localhost:8080/openapi.json - Raw JSON spec
- http://localhost:8080/openapi.yaml - Raw YAML spec
Just like Adam's Eyes of the Lord mirrors divine techniques instantly:
- Dart Frog loads all route handlers at startup
discoverRoutes()usesdart:mirrorsto find them in loaded librariesOpenApiGeneratorreflects on each handler's annotations- OpenAPI spec is built in memory
- Middleware serves Swagger UI
No code generation, no build step - just reflection. The package sees your code structure and mirrors it into an OpenAPI spec.
For fans of Record of Ragnarok, there's an alias:
final spec = Volund(title: 'My API')
.addRoutes(discoverRoutes())
.generate();⚔️
MIT
