Routing
Routes are registered in routes/api.dart (mounted under /api) and
routes/web.dart. A handler receives the Request and returns a value:
a Response, a resource, a model, a map or list (JSON), a string (text) or
null (204).
void apiRoutes(Router router) {
router
.get('/posts', (request) async {
final page = await Post.query().scope('published').latest().paginate();
return JsonResource.collection(page, PostResource.new);
})
.name('posts.index');
router
.get('/posts/{id}', (request) async {
final post = await Post.query().findOrFail(request.param<int>('id'));
return PostResource(post);
})
.name('posts.show')
.whereNumber('id');
router.group((router) {
router
.post('/posts', (request) async {
final data = request.validated();
final post = await Post.query().create(data.toMap());
return PostResource(post).withStatus(201);
})
.name('posts.store')
.validates(const StorePostRequest())
.returns(PostResource.schema, name: 'Post');
}, middleware: const ['auth', 'throttle:writes']);
}
Route features#
- Verbs:
get,post,put,patch,delete,options,any,match([...]),redirect,permanentRedirect,fallback. - Parameters:
{id}, optional{id?}, constraintswhereNumber,whereAlpha,whereUuid,whereIn,where('slug', regex). group(..., prefix:, middleware:, name:)nests freely.apiResource('photos', PhotoController())registersindex,store,show,update(PUT and PATCH) anddestroyasphotos/{photo}, namedphotos.indexand so on, exactly like Laravel.router.url('posts.show', {'id': 1})builds URLs from names; signed URLs are available for thesignedmiddleware.dart run bin/dartisan.dart route:listprints the table.
Unknown routes answer 404 {"message": "The route api/x could not be found."}. A known path with the wrong verb answers 405 with an Allow
header and Laravel's message.
Declared validation and responses#
A route can declare its input and output, so the kernel validates before the
handler runs and dartisan openapi:generate documents both:
.validates(const StorePostRequest())runs the form request (authorize()→ 403, rules → 422) before the handler;.rules({'q': 'required|string'})does the same with inline rules (query input forGET). The handler reads the result withrequest.validated(), which throws when the route declares no validation..returns(PostResource.schema, name: 'Post', shape: .paginated)documents the response:item({"data": ...}),raw,collection,paginated,simplePaginatedorcursorPaginated, wrapped in Laravel's resource and paginator envelopes. Named schemas becomecomponents.schemas..action('PostController@store')records the controller action (x-daravel-actionin OpenAPI).
rulesToJsonSchema(rules) is the conversion used for the request schema:
dotted keys nest, * becomes array items, nullable adds null, and
min/max/between/size map to the bound that fits the type.
Middleware#
Global middleware is configured in bootstrap/app.dart
(withMiddleware((m) => m.use([...]))), groups with m.group('api', [...]),
and routes with .middleware(const ['auth', 'can:update,post']). The order
is global → group → route, sorted by the middleware priority list.
Rate limiting#
Named limiters are defined in withRateLimits and applied with
throttle:<name>:
limiter.define(
'writes',
(request) => Limit.perMinute(30).by(request.ip),
);
Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; a limited
request gets 429 {"message": "Too Many Attempts."} with Retry-After and
X-RateLimit-Reset. Limits are shared by every isolate of the process
through the coordinator. Like Laravel, a window starts with the first hit
and resets decay later. With several isolates, keys with generous limits
(32 or more attempts) are served from small local leases that grow on hot
keys and give unused hits back: the limit is never exceeded (a hit leased by
one worker may briefly be unavailable to another, for at most 200 ms), and
the coordinator is no longer on every request.