Route Guards
Introduction
Route guards provide middleware for navigation in Nylo Website. They intercept route transitions and allow you to control whether a user can access a page, redirect them elsewhere, or modify the data passed to a route.
Common use cases include:
- Authentication checks -- redirect unauthenticated users to a login page
- Role-based access -- restrict pages to admin users
- Data validation -- ensure required data exists before navigation
- Data enrichment -- attach additional data to a route
Guards are executed in order before navigation occurs. If any guard returns handled, navigation stops (either redirecting or aborting).
Creating a Route Guard
Create a route guard using the Metro CLI:
metro make:route_guard auth
This generates a guard file:
import 'package:nylo_framework/nylo_framework.dart';
class AuthRouteGuard extends NyRouteGuard {
AuthRouteGuard();
@override
Future<GuardResult> onBefore(RouteContext context) async {
// Add your guard logic here
return next();
}
}
Guard Lifecycle
Every route guard has three lifecycle methods:
onBefore
Called before navigation occurs. This is where you check conditions and decide whether to allow, redirect, or abort navigation.
@override
Future<GuardResult> onBefore(RouteContext context) async {
bool isLoggedIn = await Auth.isAuthenticated();
if (!isLoggedIn) {
return redirect(HomePage.path);
}
return next();
}
Return values:
next()-- continue to the next guard or navigate to the routeredirect(path)-- redirect to a different routeabort()-- cancel navigation entirely
onAfter
Called after successful navigation. Use this for analytics, logging, or post-navigation side effects.
@override
Future<void> onAfter(RouteContext context) async {
// Log page view
Analytics.trackPageView(context.routeName);
}
onLeave
Called when the user is leaving a route. Return false to prevent the user from leaving.
@override
Future<bool> onLeave(RouteContext context) async {
if (hasUnsavedChanges) {
// Show confirmation dialog
return await showConfirmDialog();
}
return true; // Allow leaving
}
RouteContext
The RouteContext object is passed to all guard lifecycle methods and contains information about the navigation:
| Property | Type | Description |
|---|---|---|
context |
BuildContext? |
Current build context |
data |
dynamic |
Data passed to the route |
queryParameters |
Map<String, String> |
URL query parameters |
routeName |
String |
Name/path of the target route |
originalRouteName |
String? |
Original route name before transformations |
@override
Future<GuardResult> onBefore(RouteContext context) async {
// Access route information
String route = context.routeName;
dynamic routeData = context.data;
Map<String, String> params = context.queryParameters;
return next();
}
Transforming Route Context
Create a copy with different data:
// Change the data type
RouteContext<User> userContext = context.withData<User>(currentUser);
// Copy with modified fields
RouteContext updated = context.copyWith(
data: enrichedData,
queryParameters: {"tab": "settings"},
);
Guard Actions
next
Continue to the next guard in the chain, or navigate to the route if this is the last guard:
return next();
redirect
Redirect the user to a different route:
return redirect(LoginPage.path);
With additional options:
return redirect(
LoginPage.path,
data: {"returnTo": context.routeName},
navigationType: NavigationType.pushReplace,
queryParameters: {"source": "guard"},
);
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
Object |
required | Route path string or RouteView |
data |
dynamic |
null | Data to pass to the redirect route |
queryParameters |
Map<String, dynamic>? |
null | Query parameters |
navigationType |
NavigationType |
pushReplace |
Navigation method |
result |
dynamic |
null | Result to return |
removeUntilPredicate |
Function? |
null | Route removal predicate |
transitionType |
TransitionType? |
null | Page transition type |
onPop |
Function(dynamic)? |
null | Callback on pop |
abort
Cancel navigation without redirecting. The user stays on their current page:
return abort();
setData
Modify the data that will be passed to subsequent guards and the target route:
@override
Future<GuardResult> onBefore(RouteContext context) async {
User user = await fetchUser();
// Enrich the route data
setData({"user": user, "originalData": context.data});
return next();
}
Applying Guards to Routes
Add guards to individual routes in your router file:
appRouter() => nyRoutes((router) {
router.route(
HomePage.path,
(_) => HomePage(),
).initialRoute();
// Add a single guard
router.route(
ProfilePage.path,
(_) => ProfilePage(),
routeGuards: [AuthRouteGuard()],
);
// Add multiple guards (executed in order)
router.route(
AdminPage.path,
(_) => AdminPage(),
routeGuards: [AuthRouteGuard(), AdminRoleGuard()],
);
});
Group Guards
Apply guards to multiple routes at once using route groups:
appRouter() => nyRoutes((router) {
router.route(HomePage.path, (_) => HomePage()).initialRoute();
// All routes in this group require authentication
router.group(() {
return {
'prefix': '/dashboard',
'route_guards': [AuthRouteGuard()],
};
}, (router) {
router.route(DashboardPage.path, (_) => DashboardPage());
router.route(SettingsPage.path, (_) => SettingsPage());
router.route(ProfilePage.path, (_) => ProfilePage());
});
});
Guard Composition
Nylo Website provides tools to compose guards together for reusable patterns.
GuardStack
Combine multiple guards into a single reusable guard:
final protectedRoute = GuardStack([
AuthRouteGuard(),
VerifyEmailGuard(),
TwoFactorGuard(),
]);
// Use the stack on a route
router.route(
SecurePage.path,
(_) => SecurePage(),
routeGuards: [protectedRoute],
);
GuardStack executes guards in order. If any guard returns handled, the remaining guards are skipped.
ConditionalGuard
Apply a guard only when a condition is true:
router.route(
BetaPage.path,
(_) => BetaPage(),
routeGuards: [
ConditionalGuard(
condition: (context) => context.queryParameters.containsKey("beta"),
guard: BetaAccessGuard(),
),
],
);
If the condition returns false, the guard is skipped and navigation continues.
ParameterizedGuard
Create guards that accept configuration parameters:
class RoleGuard extends ParameterizedGuard<List<String>> {
RoleGuard(super.params); // params = allowed roles
@override
Future<GuardResult> onBefore(RouteContext context) async {
User? user = await Auth.user<User>();
if (user == null || !params.contains(user.role)) {
return redirect(UnauthorizedPage.path);
}
return next();
}
}
// Usage
router.route(
AdminPage.path,
(_) => AdminPage(),
routeGuards: [RoleGuard(["admin", "super_admin"])],
);
Examples
Authentication Guard
class AuthRouteGuard extends NyRouteGuard {
AuthRouteGuard();
@override
Future<GuardResult> onBefore(RouteContext context) async {
bool isAuthenticated = await Auth.isAuthenticated();
if (!isAuthenticated) {
return redirect(HomePage.path);
}
return next();
}
}
Subscription Guard with Parameters
class SubscriptionGuard extends ParameterizedGuard<List<String>> {
SubscriptionGuard(super.params);
@override
Future<GuardResult> onBefore(RouteContext context) async {
User? user = await Auth.user<User>();
bool hasAccess = params.any((plan) => user?.subscription == plan);
if (!hasAccess) {
return redirect(UpgradePage.path, data: {"plans": params});
}
setData({"user": user});
return next();
}
}
// Require premium or pro subscription
router.route(
PremiumPage.path,
(_) => PremiumPage(),
routeGuards: [
AuthRouteGuard(),
SubscriptionGuard(["premium", "pro"]),
],
);
Logging Guard
class LoggingGuard extends NyRouteGuard {
LoggingGuard();
@override
Future<GuardResult> onBefore(RouteContext context) async {
print("Navigating to: ${context.routeName}");
return next();
}
@override
Future<void> onAfter(RouteContext context) async {
print("Arrived at: ${context.routeName}");
}
}