A LifecycleComponent is a class whose public methods become middleware, guards, interceptors, catchers, or request wrappers, depending on each method's return type. It is the recommended way to write lifecycle code. For when components run and where to apply them, see the
overview.
import 'package:revali_router/revali_router.dart';
class LoadUser implements LifecycleComponent {
const LoadUser(this.users); // UserService comes from DI
final UserService users;
// Middleware: runs first and loads the user into request data
Future<MiddlewareResult> load(@Header('Authorization') String? auth, Data data) async {
if (auth != null) {
if (await users.fromToken(auth) case final user?) {
data.add<User>(user);
}
}
return const MiddlewareResult.next();
}
// Guard: runs after all middleware
GuardResult requireUser(Data data) {
if (!data.has<User>()) {
return const GuardResult.block(statusCode: 401, body: 'Sign in first');
}
return const GuardResult.pass();
}
}
import 'package:revali_router/revali_router.dart';
@LifecycleComponents([LoadUser])
@Controller('me')
class MeController {
const MeController();
@Get()
String name(@Data() User user) => user.name;
}
GET /api/me with a valid Authorization header returns 200 {"data":"Ada"}, and without one returns
401 Sign in first. The component is applied as a type (@LifecycleComponents([LoadUser])) because its constructor needs a service from DI. See
Registering Components.
Methods#
Only public, non-static methods with one of these return types are used. Every other method is ignored, so you can keep private helpers on the class.
| Return type | Role | Async form |
|---|---|---|
MiddlewareResult |
Middleware | Future<MiddlewareResult> |
GuardResult |
Guard | Future<GuardResult> |
InterceptorPreResult |
Interceptor (pre) | Mark the method async and keep the return type |
InterceptorPostResult |
Interceptor (post) | Mark the method async and keep the return type |
ExceptionCatcherResult<T> |
Exception catcher
for exceptions of type
T
|
Not supported. Catchers must be synchronous. |
WrapperResult |
Request wrapper
. Also needs a
NextResponse
parameter.
|
Already a Future<Response> |
InterceptorPreResult and InterceptorPostResult are aliases for FutureOr<void>, so an interceptor returns nothing. Write the alias rather than
void, because Revali uses the return type to recognize the role.
A class can have any number of methods, including several for the same role. They run in declaration order.
Method Parameters#
Method parameters are bound the same way as endpoint parameters:
-
Binding annotations:
@Header(),@Query(),@Param(),@Body(),@Cookie(),@Data(),@Dep(), and custom binds. See Binding. -
Implied types need no annotation:
Context,Request,Response,Headers,Data,MetaScope,Reflect,DI, and more. The full list is in Binding: Implied binding. -
The exception: in an exception catcher, a parameter of type
Treceives the exception that was thrown. -
NextResponse: in a request wrapper, it continues the rest of the pipeline.
A required binding that is missing, such as @Header('X-Key') String key when the header is absent, throws a
MissingArgumentException. Revali turns that into a 400 unless a catcher handles it. Make the parameter nullable (String? key) when the value is optional.
Constructor Parameters#
The constructor's values come from one of two places, depending on how the component is applied:
| Applied as | Constructor values come from |
|---|---|
An instance: @LoadUser(...) |
The arguments written in the annotation, which must be constants |
A type: @LifecycleComponents([LoadUser]) |
Dependency injection
.
@Dep()
is implied, so you don't write it.
|
With a type reference, you can also bind request values into the constructor with binding annotations such as
@Data() or @Header(). They are resolved per request:
class RequireRole implements LifecycleComponent {
const RequireRole(this.roles, {@Data() required this.user});
final RoleService roles; // from DI
final User user; // from request data
GuardResult check() =>
roles.isAdmin(user) ? const GuardResult.pass() : const GuardResult.block();
}
To mix constant configuration with a DI dependency in one annotation, see Registering Components.
Components Are Per-Request#
A new instance is created for every request, and for each role it runs in. Do not keep state in fields between method calls: the middleware and the guard of the same class do not share an instance. Pass values between methods, and between components, through
Data.
Classic Style#
The per-role pages also document the classic interfaces (implements Middleware, implements Guard, and so on), where one class has one fixed method (use,
protect, ...) and receives the whole Context. They still work and are useful for reusable library code, but they are the advanced option. Pick one style per feature.