Routing
Routing maps each incoming request to a handler based on its path (URI) and HTTP method.
Each route maps a request to a single handler.
Route definitions are added via RelicRouter.add(...) (or convenience methods on RelicRouter/RelicApp):
router.add(Method.get, '/my/path', myHandler);
// Or, with a convenience method:
router.get('/my/path', myHandler);
Where:
routeris an instance ofRelicRouterorRelicApp(which implementsRelicRouter).Methodis theMethodenum (e.g.Method.get,Method.post)./my/pathis a path on the server.myHandleris aHandlerthat executes when the route is matched.
The add method and its shortcuts
At the core of routing is the add method:
router.add(Method.get, '/', handler);
The convenience methods .get(), .post(), .anyOf(), and .any() call add() for you:
.get(path, handler)→.add(Method.get, path, handler).post(path, handler)→.add(Method.post, path, handler).anyOf({Method.get, Method.post}, path, handler)→ calls.add()for each method in the set..any(path, handler)→ calls.anyOf()with all HTTP methods.
Breaking down the routes
The following examples break down each route from the complete example above.
Convenience methods
These are convenience methods for the core .add() method:
Respond with Hello World! on the homepage:
app.get('/', (final req) {
return Response.ok(body: Body.fromString('Hello World!'));
});
Respond to a POST request on the root route:
app.post('/', (final req) {
return Response.ok(body: Body.fromString('Got a POST request'));
});
Respond to a PUT request to the /user route:
app.put('/user', (final req) {
return Response.ok(body: Body.fromString('Got a PUT request at /user'));
});
Respond to a DELETE request to the /user route:
app.delete('/user', (final req) {
return Response.ok(body: Body.fromString('Got a DELETE request at /user'));
});
Using the add method
This is what the convenience methods call internally:
Respond to a PATCH request using the core .add() method:
app.add(Method.patch, '/api', (final req) {
return Response.ok(body: Body.fromString('Got a PATCH request at /api'));
});
Using anyOf for multiple methods
Handle multiple HTTP methods with the same handler:
Handle both GET and POST requests to /admin:
app.anyOf({Method.get, Method.post}, '/admin', (final req) {
final method = req.method.name.toUpperCase();
return Response.ok(body: Body.fromString('Admin page - $method request'));
});
Path parameters, wildcards, and tail segments
Relic's router supports three types of variable path segments:
- Path parameters (
:id) capture named segments and are available viarequest.pathParameters.raw. - Wildcards (
*) match any single path segment but do not capture a value. - Tail segments (
/**) capture the rest of the path and expose it throughrequest.remainingPath.
Path parameters (:id)
Use a colon-prefixed name to capture a segment. Access the value with the Symbol-based key that matches the parameter name.
final app = RelicApp()
..get('/users/:id', (final Request request) {
final userId = request.pathParameters.raw[#id];
return Response.ok(
body: Body.fromString('User $userId'),
);
});
Typed path parameters
Raw path parameters are always strings, which means you need to parse them manually. Relic provides typed parameter accessors that handle parsing automatically and give you compile-time type safety.
Define a parameter accessor once, then use it to extract typed values:
// Define typed parameter accessors
const idParam = IntPathParam(#id);
const latParam = DoublePathParam(#lat);
const lonParam = DoublePathParam(#lon);
app.get('/users/:id', (final Request request) {
// Automatically parsed as int (throws if missing or invalid)
final userId = request.pathParameters.get(idParam);
return Response.ok(body: Body.fromString('User $userId'));
});
app.get('/location/:lat/:lon', (final Request request) {
// Automatically parsed as double (throws if missing or invalid)
final lat = request.pathParameters.get(latParam);
final lon = request.pathParameters.get(lonParam);
return Response.ok(body: Body.fromString('Location: $lat, $lon'));
});
You can also use the nullable variant by calling the accessor directly:
app.get('/optional/:id', (final Request request) {
final userId = request.pathParameters(idParam); // int? - null if missing
return Response.ok(body: Body.fromString('User: $userId'));
});
Relic provides these built-in typed parameter accessors:
| Accessor | Type | Description |
|---|---|---|
IntPathParam | int | Integer values (IDs, counts) |
DoublePathParam | double | Decimal values (coordinates, measurements) |
NumPathParam | num | Any numeric value |
PathParam<T> | Custom | Create your own with a custom parser |
For custom types, use PathParam<T> with your own parsing function:
// Custom enum parameter
const statusParam = PathParam<Status>(#status, Status.parse);
// Custom object parameter
const createdParam = PathParam<DateTime>(#date, DateTime.parse);
To create a reusable accessor like the built-in ones, extend PathParam<T> with a fixed decoder. The decoder must be a static function with signature T Function(String):
/// A reusable path parameter accessor for [DateTime] values.
final class DateTimePathParam extends PathParam<DateTime> {
const DateTimePathParam(final Symbol key) : super(key, DateTime.parse);
}
// Usage:
const dateParam = DateTimePathParam(#date);
// In a handler: request.pathParameters.get(dateParam) returns DateTime
Wildcards (*)
Use * to match any single segment without naming it. This is useful when the value does not matter, such as matching /files/<anything>/download.
app.get('/files/*/download', (final req) {
return Response.ok(body: Body.fromString('Downloading file...'));
});
Tail segments (/**)
Use /** at the end of a pattern to match the entire remaining path. The unmatched portion is available via request.remainingPath.
app.get('/static/**', (final req) {
final relativeAssetPath = req.remainingPath.toString();
return Response.ok(body: Body.fromString('Serve $relativeAssetPath'));
});
Tail segments are required when serving directories so that the handler knows which file the client requested. A route like /static without /** would not expose the requested child path.
Route matching and priority
When multiple routes could potentially match a request, Relic uses these rules:
-
Literal segments take priority over dynamic segments - A route with
/users/adminis tried before/users/:idwhen matching/users/admin. -
Backtracking ensures the best match - If a literal path leads to a dead end (no matching route), the router backtracks and tries dynamic alternatives.
This means you can freely combine:
- Specific routes (
/files/special/report) with catch-all routes (/files/**) - Literal and parameterized segments (
/api/v1/usersand/api/:version/items)
Route registration order does not affect matching, which makes it easy to compose routers from separate modules without worrying about ordering.
The router uses a trie data structure to provide efficient matching. Typical lookups run in O(segments) time regardless of how many routes are registered. Since each trie node is visited at most once during lookup, the worst case is still bounded by the total number of paths registered. Hence it is never worse than a linear scan.
How backtracking works
Consider these routes:
router.get('/:entity/:id', entityHandler); // Route 1
router.get('/users/:id/profile', profileHandler); // Route 2
When a request comes in for /users/789:
- The router first tries the literal
userssegment (from Route 2) - Route 2 requires a third segment
/profile, but the path ends at789 - The router backtracks and tries the parameter
:entityinstead - Route 1 matches with
entity=usersandid=789
Without backtracking, the request would fail because the router would commit to the literal users path and never consider Route 1.
Backtracking with tail segments
Tail segments (/**) act as catch-alls and benefit from backtracking:
router.get('/files/**', catchAllHandler); // Route 1
router.get('/files/special/report', reportHandler); // Route 2
/files/special/report→ matches Route 2 (exact match)/files/special/other→ backtracks to Route 1 (catch-all)
This allows you to define specific routes alongside catch-all routes, with the specific routes taking priority when they fully match.
Examples & further reading
Examples
- Basic routing example - The complete working example from this guide.
API documentation
- RelicApp class - Main application class with routing methods.
- Router class - URL router for mapping path patterns.
- Method enum - HTTP methods enumeration.