Controllers and routing annotations
import 'package:gisila/gisila.dart';
part 'users_controller.g.dart';These annotations are read at build time by the gisila generator (see Code generation). They have no effect at runtime unless you run build_runner and register the generated attachToApp method with GisilaApp.registerController.
Controller
Section titled “Controller”Marks a class as a controller.
class Controller { const Controller([String path = '']); final String path;}When to use: on every class whose methods should become routes. The path is prepended to every method path. Use one controller per resource (/users, /orders).
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
String |
'' |
Common prefix such as '/users'. May contain placeholders: '/tenants/{tenantId}'. |
A controller class with no route methods is skipped with a build warning. Controllers are instantiated once by you, so constructor arguments are app-wide dependencies; per-request state belongs in a Service.
Example
@Controller('/users')@RequireAuth()class UsersController { @Get('/{id}') Future<User> show(String id, UserService users) => users.getById(id);}See also: Controllers concept.
Base class of the HTTP method annotations. The generator recognises any subclass of gisila’s Route.
class Route { const Route(String method, String path, { int statusCode = 200, String contentType = 'application/json', RouteConfig? config, }); final String method; final String path; final int statusCode; final String contentType; final RouteConfig? config;}When to use: use the concrete subclasses below. Route itself is useful only if you want a custom verb annotation (subclass it with a const constructor).
| Parameter | Type | Default | Description |
|---|---|---|---|
method |
String |
required | HTTP verb. |
path |
String |
required | Route path relative to the controller path. Placeholders use braces: '/{id}'. |
statusCode |
int |
200 (see subclasses) |
Status of the success response when the method returns a value or void. |
contentType |
String |
'application/json' |
Declared but currently not used by the generator: value responses are always sent as application/json; charset=utf-8. Return a Response to choose another type. |
config |
RouteConfig? |
null |
Per-route rate limit, timeout, body cap, content types and more. Merged on top of @UseRouteConfig and AppConfig.defaultRouteConfig. |
HTTP method annotations
Section titled “HTTP method annotations”const Get(String path, {int statusCode = 200, String contentType, RouteConfig? config});const Post(String path, {int statusCode = 201, String contentType, RouteConfig? config});const Put(String path, {int statusCode = 200, String contentType, RouteConfig? config});const Patch(String path, {int statusCode = 200, String contentType, RouteConfig? config});const Delete(String path, {int statusCode = 204, String contentType, RouteConfig? config});const Head(String path, {int statusCode = 200, RouteConfig? config});const Options(String path, {int statusCode = 204, RouteConfig? config});| Annotation | Verb | Default success status |
|---|---|---|
Get |
GET | 200 |
Post |
POST | 201 |
Put |
PUT | 200 |
Patch |
PATCH | 200 |
Delete |
DELETE | 204 |
Head |
HEAD | 200 |
Options |
OPTIONS | 204 |
When to use: one per controller method you want to expose. Override statusCode when the default does not fit, such as 202 Accepted for work queued in the background.
Example
@Post('/imports', statusCode: 202)Future<Map<String, Object?>> startImport(ImportForm form, ImportService imports) => imports.enqueue(form);
@Get('/', config: RouteConfig(rateLimit: RateLimitConfig(requestsPerMinute: 60)))Future<List<User>> list(UserService users, {int? limit, int? offset}) => users.list(limit: limit, offset: offset);Path rules
Section titled “Path rules”- Placeholders are written
{name}and must be valid Dart identifiers. The generator converts them toshelf_router’s<name>form. - The controller path and the method path are joined with exactly one slash.
- A path ending in
/(for example@Get('/')under@Controller('/users')) is registered both as/users/and/users. - Wildcards and regex segments are not supported by the annotations. Mount a hand-written handler on a
Routerif you need them.
UseRouteConfig
Section titled “UseRouteConfig”Applies a RouteConfig to every route of a controller.
class UseRouteConfig { const UseRouteConfig(RouteConfig config); final RouteConfig config;}When to use: controller-wide defaults such as a stricter rate limit or a shorter timeout for an admin controller. For a single route, use the annotation’s config: instead.
@Controller('/admin')@UseRouteConfig(RouteConfig( rateLimit: RateLimitConfig(requestsPerMinute: 30), timeout: Duration(seconds: 3),))@Roles({'admin'})class AdminController { /* ... */ }UseMiddleware
Section titled “UseMiddleware”Attaches middleware registered by name to a controller or a route.
class UseMiddleware { const UseMiddleware(String name); final String name;}When to use: middleware that only some routes need (idempotency keys, audit logging, a tenant check). Annotations must be const, so the annotation names the middleware and you register the real one with app.registerNamedMiddleware(name, middleware). Class-level and method-level names are combined (class first, duplicates removed) and run inside the per-route pipeline, after rate limiting and body checks and before the route timeout and auth guard.
app.registerNamedMiddleware('audit', auditMiddleware(auditLog));
@Post('/')@UseMiddleware('audit')Future<Order> create(OrderForm form, OrderService orders) => orders.create(form);An unknown name throws StateError when the handler is built.
Marks a parameter as the request body.
class Body { const Body({String contentType = 'application/json', bool required = true}); final String contentType; final bool required;}When to use: rarely. The first parameter that is not a path, query, context, form or service parameter already becomes the body. Use @Body() to make that explicit, or to force a coercible type (such as a String) to be read from the body.
Marks a parameter as a query-string parameter.
class Query { const Query({String? name}); final String? name;}When to use: to read a query key whose name differs from the Dart parameter (?page_size= into pageSize), or to force a parameter to come from the query string. Parameters of type String, int, double, num, bool or DateTime that don’t match a path placeholder are query parameters already.
@Get('/')Future<List<Post>> list(@Query(name: 'page_size') int? pageSize) => ...;Marks a parameter as a path parameter.
class Path { const Path({String? name}); final String? name;}When to use: when the Dart parameter name differs from the placeholder. A parameter whose name matches a {placeholder} is bound automatically.
@Get('/{user_id}')Future<User> show(@Path(name: 'user_id') String userId) => ...;Header
Section titled “Header”Marks a parameter as a request header.
class Header { const Header({String? name}); final String? name;}When to use: to read a header into a parameter. Headers are never inferred, so the annotation is required. The value is passed as-is (not coerced), and absent headers are null, so declare the parameter as String?.
@Post('/payments')Future<Payment> charge( ChargeForm form, PaymentService payments, @Header(name: 'Idempotency-Key') String? idempotencyKey,) => payments.charge(form, idempotencyKey: idempotencyKey);Parameter binding rules
Section titled “Parameter binding rules”For each method parameter, the generator picks the first rule that matches:
| # | Parameter | Bound from |
|---|---|---|
| 1 | Annotated with @Path, @Query, @Header or @Body |
That source. |
| 2 | Type Request (Shelf) |
The raw request as it reached the route. |
| 3 | Type RequestContext |
The request context. |
| 4 | Subtype of Form |
bindForm(request, YourForm.new): parsed and validated. Needs a zero-argument constructor. |
| 5 | Subtype of Service |
ctx.service<YourService>(). Register it with app.registerService. |
| 6 | Name matches a {placeholder} in the full path |
Path parameter, coerced to its type. |
| 7 | Type String, int, double, num, bool or DateTime (nullable or not) |
Query parameter of the same name, coerced with coerce. |
| 8 | Anything else, first such parameter | The request body (see below). |
| 9 | Anything else, after a body was already taken | Build error. |
Positional and named parameters both work.
Query parameters are required when the parameter is non-nullable and not optional. An empty value (?limit=) counts as missing for every type except String. A value that fails to parse is a 400.
Body parameter types
Section titled “Body parameter types”| Parameter type | What the generated code does |
|---|---|
FormData |
readFormBody: url-encoded or multipart only. |
UploadedFile |
Reads the multipart body and takes the file whose field name equals the parameter name. Missing file is a 400 when required. |
List<T> where T has fromJson(Map) |
readJsonBody, then T.fromJson per element. JSON only. |
Other List |
readJsonBody, passed through. JSON only. |
Class with fromForm(Map) and fromJson(Map) |
readBody; fromForm for form bodies, fromJson for JSON. |
Class with only fromForm(Map) or only fromJson(Map) |
readBody, then that factory (for any content type). |
Map / Map<String, dynamic> |
readBody, cast to Map<String, dynamic>. |
| Anything else | The raw value from readBody. |
A TypeError or FormatException while binding any parameter (such as a JSON object missing a field your fromJson casts) becomes a 400 with code bad_request. Exceptions thrown by your method itself are not converted.
Return values
Section titled “Return values”| Method returns | Response |
|---|---|
Response / Future<Response> |
Sent unchanged. Use it for redirects, files, streams, custom headers or non-JSON bodies. |
void / Future<void> |
Empty body with the annotation’s statusCode (so 200 for @Get, 204 for @Delete). |
| Anything else | jsonResponse(body: result, statusCode: statusCode): objects are encoded through their toJson(), DateTime as ISO 8601, enums as their name, and lists and maps recursively. |
See also: Build a CRUD API, Code generation concept.