roux
Lightweight, fast router for Dart with expressive pathname syntax.
Install
dart pub add roux
With Flutter:
flutter pub add roux
Usage
import 'package:roux/roux.dart';
final router = Router<String>(
routes: {
'/': 'root',
'/users/all': 'users-all',
'/users/:id': 'users-id',
'/users/*': 'users-one-segment',
'/users/**:wildcard': 'users-wildcard',
'/**:wildcard': 'global-fallback',
},
);
final match = router.match('/users/123');
print(match?.data); // users-id
print(match?.params); // {id: 123}
final stack = router.matchAll('/users/123');
print(
stack.map((match) => match.data),
); // (global-fallback, users-wildcard, users-id, users-one-segment)
Register incrementally:
final router = Router<String>();
router.add('/posts', 'posts');
router.add('/posts/:id', 'post-detail');
router.add('/posts/**:rest', 'post-fallback');
Method-specific routes use method: on add, addAll, match, and
matchAll.
Route Patterns
Supported route shapes:
| Syntax | Meaning | Example |
|---|---|---|
/users/all |
Exact static route | /users/all |
/users/:id |
Named single-segment param | /users/:id |
/users/* |
Single-segment wildcard | /users/* |
/users/**:rest |
Named remainder wildcard | /users/**:rest |
/users/** |
Unnamed remainder wildcard | /users/** |
/files/:name.:ext |
Embedded params inside one segment | /files/:name.:ext |
/files/file-*.png |
Embedded wildcard inside one segment | /files/file-*.png |
/users/:id(\\d+) |
Named regex param | /users/:id(\\d+) |
/users/:id? |
Optional param segment | /users/:id? |
/files/:path+ |
One-or-more repeated segments | /files/:path+ |
/files/:path* |
Zero-or-more repeated segments | /files/:path* |
/foo{bar} |
Mandatory non-capturing group | /foo{bar} |
/book{s}? |
Optional non-capturing group | /book{s}? |
/users{/:id}? |
Optional grouped suffix | /users{/:id}? |
/blog/:id(\\d+){-:title}? |
Mixed params, regex, and optional group | /blog/:id(\\d+){-:title}? |
Rules:
- Route patterns must start with
/. *matches exactly one segment.**and**:namematch the remaining path and must be the final segment.rouxroutes pathnames only. It does not parseprotocol,hostname,search, orhash.
Input Processing
Path preprocessing is conservative by default:
final router = Router<String>(
caseSensitive: true,
decodePath: false,
normalizePath: false,
);
| Option | Default | Effect |
|---|---|---|
caseSensitive |
true |
Match paths with case sensitivity. |
decodePath |
false |
Leave %xx sequences untouched. |
normalizePath |
false |
Repeated /, . and .. are not normalized; empty segments are rejected. |
caseSensitive: falseignores case for static and compiled matching while preserving original parameter values.decodePath: truedecodes%xxsequences before matching. Invalid encodings fail closed and return no match.- With
normalizePath: false, repeated/create empty segments and fail closed. For example,match('/a//b')returns no match. normalizePath: truecollapses repeated/, removes.segments, resolves.., and rejects paths that would escape above/.
Lookup processing order:
- URL decoding, if enabled
- Path normalization, if enabled
- Route matching
Because decoding runs first, decodePath: true can change segment boundaries.
For example, /a%2Fb becomes /a/b before matching.
match(...) priority is path-dependent, but the broad rules are:
- Exact static routes win first.
- Structured dynamic routes participate in single-match precedence too. This includes embedded patterns, regex params, grouped patterns, optional segments, and repeated segments.
- Regex and shell-style structured patterns can beat plain
:paramroutes. - Plain
:paramroutes beat single-segment wildcards. - Single-segment wildcards beat remainder wildcards.
- Global fallback is always last.
Examples:
/users/:id(\\d+)beats/users/:idfor/users/42./files/file-*.pngbeats/files/:name.:extfor/files/file-a.png./users/:idbeats/users{/:id}?for/users/42.
matchAll(...) order is always less specific to more specific. Broadly:
- remainder routes
- single-segment dynamic routes
- structured dynamic routes, including embedded, regex, grouped, optional, and repeated patterns
- exact static routes
Differences from URLPattern
rouxroutes pathname only. There is noprotocol,hostname,port,search,hash, orbaseURLmatching.- Patterns must start with
/. *matches exactly one segment.**and**:namematch the remaining path. This differs fromURLPattern, where*is more permissive.- URL decoding is configurable and off by default with
decodePath: false. - Path normalization is configurable and off by default with
normalizePath: false. - Case sensitivity is configurable through
caseSensitive. - Trailing slash on lookup input is ignored, so
/usersand/users/match the same route.
Duplicate Policy
Duplicate route handling is configurable at both router and call level:
final router = Router<String>(
duplicatePolicy: DuplicatePolicy.replace,
caseSensitive: false,
decodePath: true,
normalizePath: true,
routes: {'/users/:id': 'first'},
);
router.add('/users/:id', 'second');
print(router.match('/users/42')?.data); // second
Available policies:
DuplicatePolicy.rejectthrows on duplicate route shapes.DuplicatePolicy.replacekeeps the latest retained entry.DuplicatePolicy.keepFirstkeeps the earliest retained entry.DuplicatePolicy.appendretains all entries in registration order.
Per-call overrides are also supported:
router.add(
'/users/:id',
'third',
duplicatePolicy: DuplicatePolicy.keepFirst,
);
To retain multiple handlers in the same normalized slot:
final router = Router<String>(duplicatePolicy: DuplicatePolicy.append);
router.add('/**:wildcard', 'global-logger');
router.add('/**:wildcard', 'root-scope-middleware');
print(router.match('/users/42')?.data); // global-logger
print(
router.matchAll('/users/42').map((match) => match.data),
); // (global-logger, root-scope-middleware)
Parameter-name drift remains a hard error under all policies. For example,
/users/:id and /users/:name still conflict.
Benchmarks
Benchmarks are split into three single-scenario scripts. Each process runs one target only to avoid cross-target warmup bias.
Primary comparison benchmark:
dart run bench/aligned_feature.dart roux 500 50000 4096
dart run bench/aligned_feature.dart relic 500 50000 4096
This is the benchmark to use for ongoing roux vs relic comparison.
It compares a shared dirty-normalized pathname contract with params consumed.
roux runs with normalizePath: true.
relic uses its built-in always-normalized lookup behavior.
Dynamic paths are intentionally reused under a bounded cardinality so the
comparison measures routing work instead of relic intern-cache thrash.
Minimal feature set:
dart run bench/minimal_feature.dart roux 500 50000 4096
dart run bench/minimal_feature.dart relic 500 50000 4096
Maximal feature contract:
dart run bench/maximal_feature.dart roux 500 50000 4096
dart run bench/maximal_feature.dart relic 500 50000 4096
Cache-thrash stress benchmark:
dart run bench/cache_thrash_feature.dart roux 500 50000
dart run bench/cache_thrash_feature.dart relic 500 50000
License
MIT. See LICENSE.