Skip to content

RouteConfig and the route runtime

import 'package:gisila/gisila.dart';

Declarative, per-route configuration. An empty RouteConfig changes nothing, so configs can be layered.

class RouteConfig {
const RouteConfig({
RateLimitConfig? rateLimit,
Duration? timeout,
int? maxBodyBytes,
Set<String>? contentTypes,
CorsConfig? cors,
List<Middleware> middleware = const [],
bool requireAuth = false,
Set<String> permissions = const {},
Set<String> anyPermission = const {},
Set<String> roles = const {},
int? statusCode,
bool isPublic = false,
});
static const empty = RouteConfig();
RouteConfig merge(RouteConfig? other);
RouteConfig operator +(RouteConfig? other);
RouteConfig copyWith({...});
bool get hasAuthGuards;
}

When to use: when one route (or one controller) needs different limits from the rest: a tight rate limit on login, a longer timeout on a report, a larger body cap on uploads, JSON-only input. Pass it as config: on a route annotation, in @UseRouteConfig on a controller, or as AppConfig.defaultRouteConfig for every route.

Field Type Default Description
rateLimit RateLimitConfig? null A limiter just for this route (each route gets its own buckets). Applies in addition to AppConfig.rateLimit, so a request must pass both.
timeout Duration? null Answer 504 if the route (its middleware, guard and handler) takes longer. Applies in addition to AppConfig.requestTimeout; the shorter wins.
maxBodyBytes int? null Reject bodies above this size with 413. Applies in addition to AppConfig.maxRequestBodyBytes.
contentTypes Set<String>? null Allowed Content-Type values (without parameters) for POST, PUT and PATCH. Anything else, including a missing header, is a 415.
cors CorsConfig? null CORS headers for this route’s responses. Pre-flight OPTIONS requests only reach a route that is itself an OPTIONS route, so keep pre-flights on the app-wide AppConfig.cors.
middleware List<Middleware> [] Extra Shelf middleware, run in list order after the built-in per-route checks and before the timeout and guard. Middleware isn’t const, so annotations can’t hold it; use @UseMiddleware there.
requireAuth bool false Require an authenticated principal.
permissions Set<String> {} The principal must hold every one.
anyPermission Set<String> {} The principal must hold at least one.
roles Set<String> {} The principal must hold at least one.
statusCode int? null Carried through merges, but the generated code takes the success status from the annotation’s statusCode, so setting it here has no effect on generated routes.
isPublic bool false Turns off every auth guard on the route. @Public() sets it.

You normally set the guard fields through the permission annotations rather than by hand.

The generator merges, from least to most specific:

  1. AppConfig.defaultRouteConfig
  2. @UseRouteConfig on the class
  3. guards from @RequireAuth, @Permissions, @AnyPermission, @Roles, @Public (class and method combined)
  4. the annotation’s config:
  5. @UseMiddleware names (class, then method)
RouteConfig merge(RouteConfig? other)

Returns a new config where scalar fields come from other when set and fall back to this; the permission, any-permission and role sets are unioned; middleware lists are concatenated. When other.isPublic is true, the result is public and all guards are cleared. A public config stays public through later layers unless one of them adds a guard itself. merge(null) returns this.

Shorthand for merge: base + override.

Returns a copy with the given fields replaced (no union, no public logic). Prefer merge.

true when the config is not public and requires auth or lists any permission or role. When true, the route runs the authenticator and permission checks.

Example

@Controller('/auth')
class AuthController {
@Post('/login', config: RouteConfig(
rateLimit: RateLimitConfig(requestsPerMinute: 10, burstSize: 5),
contentTypes: {'application/json'},
maxBodyBytes: 16 * 1024,
))
Future<Map<String, Object?>> login(LoginForm form, AuthService auth) =>
auth.login(form.email.value!, form.password.value!);
@Get('/report', config: RouteConfig(timeout: Duration(seconds: 60)))
@Roles({'admin'})
Future<Report> report(ReportService reports) => reports.build();
}

See also: Rate limiting guide, Request lifecycle.

Builds the Shelf handler for one route from its effective config and a typed handler. Generated attachToApp methods call it for every route.

Handler gisilaRoute({
required GisilaApp app,
required RouteConfig config,
required GisilaRouteHandler handler,
String? routeId,
})

When to use: to register a hand-written route that still gets gisila’s per-route pipeline (guards, limits, timeout) without the generator, for example inside a custom ControllerAttacher.

Parameter Type Default Description
app GisilaApp required Supplies defaultRouteConfig, the authenticator, the permission resolver, trusted proxies and the rate-limit store.
config RouteConfig required Merged on top of app.config.defaultRouteConfig.
handler GisilaRouteHandler required Receives the request context and returns the response.
routeId String? null 'METHOD /path/<param>'. Names the route’s limiter in a shared rate-limit store and labels metrics with the path template. Without it, metrics label the route other.

The per-route pipeline, outermost first: CORS, rate limit, content-type check, body cap, custom middleware, timeout, auth guard, handler. Guard failures answer 401 (unauthorized) or 403 (forbidden) directly.

app.registerController(attacher: (app, router, {prefix = ''}) {
router.get(
'$prefix/version',
gisilaRoute(
app: app,
config: const RouteConfig(timeout: Duration(seconds: 1)),
routeId: 'GET $prefix/version',
handler: (ctx) => jsonResponse(body: {'version': '1.2.3'}),
),
);
});
typedef GisilaRouteHandler = FutureOr<Response> Function(RequestContext ctx);

The typed handler gisilaRoute wraps. ctx.request is the request as it reached the handler.

Runs authentication and permission checks against the GisilaApp attached to the request.

Future<GuardResult> guardRequest(
Request request, {
required bool requireAuth,
Set<String> permissions = const {},
Set<String> anyPermission = const {},
Set<String> roles = const {},
})

When to use: in a hand-written Shelf handler mounted inside a GisilaApp that must enforce the same guards as generated routes. Generated code no longer calls it (guards run in gisilaRoute). It returns a RequestContext with principal set on success, or a ready 401/403 Response on failure. Outside a GisilaApp pipeline it fails closed with a 500 when any guard is requested.

Future<Response> adminStats(Request request) async {
final result = await guardRequest(request, requireAuth: true, roles: {'admin'});
if (result is Response) return result;
final ctx = result as RequestContext;
return jsonResponse(body: {'user': ctx.principal!.id});
}
typedef GuardResult = Object;

Return type of guardRequest: a RequestContext or a Response.