# Routing
- [Introduction](#introduction)
- [Basic Routing](#basic-routing)
- [Route Parameters](#route-parameters)
- [Query Parameters](#query-parameters)
- [Named Routes](#named-routes)
- [Route Groups](#route-groups)
- [Middleware](#middleware)
- [Prefixes](#prefixes)
- [Layouts (Shell Routes)](#layouts-shell-routes)
- [Context-Free Navigation](#context-free-navigation)
- [Route Middleware](#route-middleware)
- [URL Strategy](#url-strategy)
- [Navigator Observers](#navigator-observers)
## Introduction
The most basic Magic routes accept a URI and a closure, providing a very simple and expressive method of defining routes and behavior without complicated routing configuration files.
All routes for your application are defined in the `lib/routes` directory. These files are loaded by the `RouteServiceProvider`, which is included in your `config/app.dart` by default.
## Basic Routing
The most basic route definitions involve calling a method on the `MagicRoute` facade:
```dart
MagicRoute.page('/', () => HomePage());
```
### Route Methods
Use the `page` method to define full-screen page routes:
```dart
// Simple page
MagicRoute.page('/greeting', () => Text('Hello World'));
// Controller action
MagicRoute.page('/dashboard', () => DashboardController.instance.index());
// Inline widget
MagicRoute.page('/about', () => AboutPage());
```
### The Initial Route
Configure your application's initial route via `MagicApplication`:
```dart
runApp(
MagicApplication(
initialRoute: '/dashboard',
// ...
),
);
```
## Route Parameters
### Required Parameters
Sometimes you need to capture segments of the URI. For example, to capture a user's ID:
```dart
MagicRoute.page('/user/:id', (id) {
return UserProfileView(userId: id);
});
```
You may define as many route parameters as required:
```dart
MagicRoute.page('/posts/:postId/comments/:commentId', (postId, commentId) {
return CommentView(postId: postId, commentId: commentId);
});
```
> [!NOTE]
> Magic uses `:param` syntax (like Express.js) instead of Laravel's `{param}` syntax.
## Query Parameters
Query parameters are the key-value pairs that appear after the `?` in a URL (e.g., `/search?q=flutter&page=2`). Magic provides the `Request` facade to read them from the current route.
### Reading Query Parameters
Use `Request.query()` to retrieve a single query parameter by key. It returns `null` when the key is absent:
```dart
// URL: /search?q=flutter&page=2
final term = Request.query('q'); // 'flutter'
final page = Request.query('page'); // '2'
final sort = Request.query('sort'); // null
```
Use `Request.queryParams` to retrieve all query parameters as a `Map`:
```dart
// URL: /search?q=flutter&sort=desc
final params = Request.queryParams;
// {'q': 'flutter', 'sort': 'desc'}
```
### Navigating With Query Parameters
Pass a `query` map to `MagicRoute.to()` or `MagicRoute.toNamed()` to append query parameters to the URL:
```dart
// By path
MagicRoute.to('/search', query: {'q': 'flutter'});
// By name
MagicRoute.toNamed('search', query: {'q': 'flutter', 'page': '2'});
```
> [!NOTE]
> Query parameters are always `String` values. Convert to other types after reading (e.g., `int.tryParse(Request.query('page') ?? '')`).
## Named Routes
Named routes allow convenient generation of URLs or redirects for specific routes. Specify a name by chaining the `name` method:
```dart
MagicRoute.page('/user/profile', () => ProfileView())
.name('profile');
MagicRoute.page('/user/:id', (id) => UserView(id: id))
.name('user.show');
```
### Navigating To Named Routes
Once you have assigned a name to a route, you may use it when navigating:
```dart
// Navigate to named route
MagicRoute.toNamed('profile');
// With path parameters
MagicRoute.toNamed('user.show', params: {'id': '42'});
// With query parameters
MagicRoute.toNamed('search', query: {'q': 'flutter'});
```
## Route Groups
Route groups allow you to share route attributes, such as middleware or prefixes, across multiple routes.
### Middleware
Assign middleware to all routes within a group:
```dart
MagicRoute.group(
middleware: ['auth'],
routes: () {
MagicRoute.page('/dashboard', () => DashboardView());
MagicRoute.page('/profile', () => ProfileView());
},
);
```
### Prefixes
Add a path prefix to all routes in a group:
```dart
MagicRoute.group(
prefix: '/admin',
middleware: ['auth', 'admin'],
routes: () {
MagicRoute.page('/', () => AdminDashboard()); // /admin
MagicRoute.page('/users', () => AdminUsers()); // /admin/users
MagicRoute.page('/settings', () => AdminSettings()); // /admin/settings
},
);
```
### Nested Groups
Groups can be nested. Child groups inherit parent attributes:
```dart
MagicRoute.group(
prefix: '/admin',
middleware: ['auth'],
routes: () {
MagicRoute.group(
prefix: '/users',
routes: () {
MagicRoute.page('/', () => UserList()); // /admin/users
MagicRoute.page('/:id', (id) => UserShow(id: id)); // /admin/users/:id
},
);
},
);
```
### Layouts (Shell Routes)
Assign a persistent layout to all routes within a group. The layout persists while child pages change—perfect for tab bars, navigation rails, and sidebars:
```dart
MagicRoute.group(
layout: (child) => AppLayout(child: child),
middleware: ['auth'],
routes: () {
MagicRoute.page('/', () => DashboardView());
MagicRoute.page('/monitors', () => MonitorsView());
MagicRoute.page('/settings', () => SettingsView());
},
);
```
Your layout widget should accept and render the `child` parameter:
```dart
class AppLayout extends StatelessWidget {
final Widget child;
const AppLayout({required this.child, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
AppSidebar(),
Expanded(child: child), // Child pages render here
],
),
);
}
}
```
> [!TIP]
> Use layouts for any UI that should persist across page navigation, such as sidebars, bottom navigation bars, or headers.
## Context-Free Navigation
You may navigate from anywhere in your application—controllers, services, or pure Dart classes—without needing `BuildContext`:
```dart
// Replace current route
MagicRoute.to('/dashboard');
// Push onto navigation stack (back button works)
MagicRoute.push('/details');
// Go back
MagicRoute.back();
// Go back with an explicit fallback path
MagicRoute.back(fallback: '/home');
// Replace current route (no new history entry)
MagicRoute.replace('/home');
// With query parameters
MagicRoute.to('/search', query: {'q': 'flutter'});
```
### Cross-Shell Back Navigation
`MagicRoute.back()` works reliably even when navigating across shell routes (layouts). Magic maintains a lightweight history stack automatically—no setup required. When the standard pop is not possible, it falls back to the last tracked history entry.
Pass an optional `fallback` path to control where navigation lands when the history stack is empty:
```dart
// Falls back to '/dashboard' if there is no navigation history
MagicRoute.back(fallback: '/dashboard');
```
> [!NOTE]
> The history stack is populated automatically by `MagicRoute.to()` and `MagicRoute.toNamed()`. `replace()` swaps the last entry without growing the stack, so back navigation after a replace lands at the entry before the replace.
### From Controllers
```dart
class AuthController extends MagicController {
Future logout() async {
await Auth.logout();
MagicRoute.to('/login'); // No context needed!
}
}
```
## Route Middleware
Assign middleware to individual routes using the `middleware` method:
```dart
MagicRoute.page('/profile', () => ProfileView())
.middleware(['auth']);
MagicRoute.page('/admin', () => AdminPanel())
.middleware(['auth', 'admin']);
```
See the [Middleware documentation](/basics/middleware) for details on creating custom middleware.
## URL Strategy
Flutter web defaults to hash-based URLs (`/#/path`). Magic can enable clean path-based URLs (`/path`) via config — no code changes needed elsewhere.
```dart
'routing': {
'url_strategy': 'path', // 'path' | 'hash' | null (default: null — hash strategy)
},
```
> [!NOTE]
> This setting has no effect on iOS, Android, or desktop — it is web-only.
When using path-based URLs, your web server must serve `index.html` for all routes. Example nginx config:
```nginx
location / {
try_files $uri $uri/ /index.html;
}
```
## Navigator Observers
Register `NavigatorObserver` instances for analytics, monitoring, or performance tracking. Observers must be added before the router is built (typically in your `RouteServiceProvider`):
```dart
class RouteServiceProvider extends ServiceProvider {
@override
Future boot() async {
// Add observers before registering routes
MagicRouter.instance.addObserver(SentryNavigatorObserver(
enableAutoTransactions: true,
setRouteNameAsTransaction: true,
));
MagicRouter.instance.addObserver(FirebaseAnalyticsObserver(
analytics: FirebaseAnalytics.instance,
));
registerAppRoutes();
}
void registerAppRoutes() {
MagicRoute.page('/', () => HomePage());
// ...
}
}
```
Observers are passed directly to GoRouter and receive all navigation events (`didPush`, `didPop`, `didReplace`, `didRemove`).
> [!NOTE]
> Observers must be registered before `routerConfig` is accessed. Adding observers after the router is built throws a `StateError`.
## Page Titles
Magic provides automatic page title management via `SystemChrome.setApplicationSwitcherDescription` — updates the browser tab title on web and the app switcher description on mobile.
### Title Suffix
Set a global suffix via `MagicApplication`:
```dart
MagicApplication(
title: 'My App',
titleSuffix: 'Kodizm.AI',
)
```
Page titles render as `"Dashboard - Kodizm.AI"`. The suffix is only applied to route-level and override titles — when no page title is set, the fallback app title is shown without suffix.
### Static Route Titles
Assign titles to routes using the fluent `.title()` method:
```dart
MagicRoute.page('/dashboard', () => DashboardPage())
.name('dashboard')
.title('Dashboard')
.middleware(['auth']);
```
The title is set automatically when the route becomes active.
### Dynamic Titles with MagicTitle Widget
For data-dependent titles that resolve after the route mounts, wrap your widget with `MagicTitle`:
```dart
class ProjectPage extends StatelessWidget {
final String projectName;
const ProjectPage({super.key, required this.projectName});
@override
Widget build(BuildContext context) {
return MagicTitle(
title: projectName,
child: Scaffold(
appBar: AppBar(title: Text(projectName)),
body: ProjectContent(),
),
);
}
}
```
`MagicTitle` sets the title on mount, updates on rebuild, and clears on dispose (falling back to the route title).
### Imperative Title API
Set or read the title from anywhere — controllers, services, callbacks:
```dart
// Set title imperatively
MagicRoute.setTitle('User Profile — John');
// Read the current title (without suffix)
final title = MagicRoute.currentTitle;
```
### Title Resolution Priority
Highest to lowest:
1. `MagicTitle` widget / `MagicRoute.setTitle()` — explicit override
2. `RouteDefinition.title()` — static route title
3. `MagicApplication.title` — app-level fallback
When a higher-priority source is cleared (e.g., `MagicTitle` disposes), the next level takes over automatically.