RouterOptions - greydragon888/real-router GitHub Wiki
The Options interface defines configuration options for the router. These options control default behavior, URL handling, query parameter parsing, logging, and other router-wide settings.
interface Options {
defaultRoute: string | DefaultRouteCallback;
defaultParams: Params | DefaultParamsCallback;
trailingSlash: "strict" | "never" | "always" | "preserve";
caseSensitive: boolean;
urlParamsEncoding: "default" | "uri" | "uriComponent" | "none";
queryParamsMode: QueryParamsMode;
queryParams?: QueryParamsOptions;
allowNotFound: boolean;
rewritePathOnMatch: boolean;
logger?: Partial<LoggerConfig>;
limits?: Partial<LimitsConfig>;
}
type DefaultRouteCallback<Dependencies = object> = (
getDependency: <K extends keyof Dependencies>(name: K) => Dependencies[K],
) => string;
type DefaultParamsCallback<Dependencies = object> = (
getDependency: <K extends keyof Dependencies>(name: K) => Dependencies[K],
) => Params;| Property | Type | Default | Description |
|---|---|---|---|
defaultRoute |
string | DefaultRouteCallback |
"" |
Route to navigate to when no initial path provided or route not found |
Accepts either a static string or a callback function that dynamically computes the route name using router dependencies.
// Static value
const router = createRouter(routes, {
defaultRoute: "home",
});
// On start with empty path, navigates to "home"
await router.start("");import { createRouter } from "@real-router/core";
import { getDependenciesApi } from "@real-router/core/api";
// Dynamic callback — resolved at point of use (start, navigateToDefault)
const router = createRouter<MyDependencies>(routes, {
defaultRoute: (getDependency) => {
const role = getDependency("userRole");
return role === "admin" ? "admin.dashboard" : "home";
},
});
getDependenciesApi(router).set("userRole", "admin");
await router.start(""); // navigates to "admin.dashboard"Note: Callbacks are never cached — they are re-evaluated on every
start()andnavigateToDefault()call.getOptions().defaultRoutereturns the function reference, not the resolved value.
| Property | Type | Default | Description |
|---|---|---|---|
defaultParams |
Params | DefaultParamsCallback |
{} |
Default parameters for the default route |
Accepts either a static Params object or a callback function that dynamically computes the parameters using router dependencies.
// Static value
const router = createRouter(routes, {
defaultRoute: "users",
defaultParams: { page: "1", sort: "name" },
});// Dynamic callback
const router = createRouter<MyDependencies>(routes, {
defaultRoute: "users.view",
defaultParams: (getDependency) => ({
id: getDependency("currentUserId"),
}),
});
getDependenciesApi(router).set("currentUserId", "42");
await router.navigateToDefault(); // navigates to "users.view" with { id: "42" }| Property | Type | Default | Description |
|---|---|---|---|
trailingSlash |
"strict" | "never" | "always" | "preserve" |
"preserve" |
How to handle trailing slashes in URLs |
| Value | Behavior |
|---|---|
"strict" |
Route must match exactly as defined |
"never" |
Always remove trailing slash |
"always" |
Always add trailing slash |
"preserve" |
Keep the source path's trailing-slash choice |
const router = createRouter(routes, {
trailingSlash: "never",
});
// /users/ becomes /usersWhen both are set (also the defaults), matchPath() rewrites the path via the matched route pattern — but the source path's trailing-slash choice is re-attached afterwards. Example:
// defaults: trailingSlash: "preserve", rewritePathOnMatch: true
api.matchPath("/users/"); // state.path === "/users/" — trailing kept
api.matchPath("/users"); // state.path === "/users" — no trailing addedRewrite semantics (forwardTo, encoders, defaultParams merge) still apply — only trailing-slash behaviour is respected. This preserves the contract promised by "preserve".
| Property | Type | Default | Description |
|---|---|---|---|
caseSensitive |
boolean |
false |
Whether route names are case-sensitive |
const router = createRouter(routes, {
caseSensitive: true,
});
// "Users" and "users" are different routes| Property | Type | Default | Description |
|---|---|---|---|
urlParamsEncoding |
"default" | "uri" | "uriComponent" | "none" |
"default" |
How to encode URL parameters |
| Value | Behavior |
|---|---|
"default" |
Standard encoding |
"uri" |
URI encoding (encodeURI) |
"uriComponent" |
Component encoding (encodeURIComponent) |
"none" |
No encoding |
| Property | Type | Default | Description |
|---|---|---|---|
queryParamsMode |
QueryParamsMode |
"loose" |
How to handle query parameters |
type QueryParamsMode = "default" | "strict" | "loose";| Value | Behavior |
|---|---|
"default" |
Standard query param handling |
"strict" |
Only declared query params are parsed |
"loose" |
All query params are parsed |
| Property | Type | Default | Description |
|---|---|---|---|
queryParams |
QueryParamsOptions |
undefined |
Query parameter parsing options |
interface QueryParamsOptions {
arrayFormat?: "none" | "brackets" | "index" | "comma";
booleanFormat?: "none" | "auto" | "empty-true";
nullFormat?: "default" | "hidden";
numberFormat?: "none" | "auto";
}| Value | Example URL | Parsed Result |
|---|---|---|
"none" |
?a=1&a=2 |
{ a: ["1", "2"] } |
"brackets" |
?a[]=1&a[]=2 |
{ a: ["1", "2"] } |
"index" |
?a[0]=1&a[1]=2 |
{ a: ["1", "2"] } |
"comma" |
?a=1,2 |
{ a: ["1", "2"] } |
| Value | Encoded (build) |
Decoded (parse) |
|---|---|---|
"auto" (default) |
true → ?flag=true, false → ?flag=false
|
?flag=true → { flag: true }, ?flag → { flag: null }
|
"none" |
true → ?flag=true
|
?flag=true → { flag: "true" } (string), ?flag → { flag: null }
|
"empty-true" |
true → ?flag, false → ?flag=false
|
?flag → { flag: true }
|
| Value | Encoded (build({ key: null })) |
Decoded (parse("?key")) |
|---|---|---|
"default" |
?key (key-only) |
{ key: null } (via booleanFormat) |
"hidden" |
(stripped — no segment) | — |
?flag vs ?flag= are distinct: ?flag parses to null (absent value), ?flag= parses to "" (explicit empty value). Three-state expressiveness.
| Value | Example URL | Parsed Result |
|---|---|---|
"none" |
?page=1 |
{ page: "1" } |
"auto" |
?page=1 |
{ page: 1 } |
Detects integers and decimals (e.g., 42, 12.5). Non-numeric strings are unaffected. No encoding change — numbers are serialized identically regardless of format.
const router = createRouter(routes, {
queryParams: { numberFormat: "auto" },
});
// /users?page=3&limit=20&sort=name
// → state.params: { page: 3, limit: 20, sort: "name" }Roundtrip gotcha: numberFormat: "auto" means { id: "42" } (string) round-trips as { id: 42 } (number) after a navigate — type changes silently. If you need strings to remain strings, set numberFormat: "none" or use @real-router/search-schema-plugin with explicit Zod types.
router.navigate() and router.buildPath() strip undefined values from params before URL building and state storage. This is enforced at the core boundary (not by the query-string engine), so it applies regardless of queryParams configuration:
router.navigate("search", { q: "hello", page: undefined, sort: null, filter: "" });
// URL: /search?q=hello&sort&filter=
// state.params: { q: "hello", sort: null, filter: "" } // "page" absentnull and "" are NOT stripped — they produce distinct URL segments. See navigate / buildPath for the full contract, and Params Contract in core README for the complete input → URL → state.params mapping.
| Property | Type | Default | Description |
|---|---|---|---|
allowNotFound |
boolean |
true |
Allow navigation to non-existent routes |
const router = createRouter(routes, {
allowNotFound: true,
});
// Navigation to unknown route creates "@@router/UNKNOWN_ROUTE" state
// instead of throwing ROUTE_NOT_FOUND errorWhen true (default), unmatched paths during start() and popstate events produce UNKNOWN_ROUTE state via navigateToNotFound(). The unmatched URL is preserved.
When false, every entry point treats an unmatched URL as an error — consistently across start(), popstate (browser-plugin / hash-plugin), and Navigation API events (navigation-plugin):
-
router.start(path)rejects its returned Promise withROUTE_NOT_FOUND. - Popstate events emit
$$error(reaching theonTransitionErrorplugin hook) and roll the browser URL back to the last-known router state. Router state is unchanged. - Navigation API
navigateevents do the same, with URL rollback handled automatically byevent.intercept()rejection.
defaultRoute is not consulted as a fallback in strict mode — it is only the target of an explicit router.navigateToDefault() call. If you want strict-mode unmatched URLs to redirect to the default, wire it up in userland:
router.usePlugin(() => ({
onTransitionError(_toState, _fromState, err) {
if (err.code === "ROUTE_NOT_FOUND") {
void router.navigateToDefault({ replace: true });
}
},
}));| Property | Type | Default | Description |
|---|---|---|---|
rewritePathOnMatch |
boolean |
true |
Rewrite state.path on successful match to the canonical form built from the matched route's pattern |
When true (the default), matchPath() returns state.path rebuilt via buildPath() — applying forwardTo aliases, encoders, defaultParams merging, and trailing-slash normalization per trailingSlash. When false, state.path is the original URL verbatim.
See trailingSlash: "preserve" for how these two options interact (the source path's trailing-slash choice wins).
| Property | Type | Default | Description |
|---|---|---|---|
logger |
Partial<LoggerConfig> |
undefined |
Logger configuration for centralized logging |
interface LoggerConfig {
level: "all" | "warn-error" | "error-only" | "none";
callback?: (
level: "log" | "warn" | "error",
context: string,
message: string,
) => void;
callbackIgnoresLevel?: boolean;
}| Level | log() |
warn() |
error() |
|---|---|---|---|
"all" |
✅ | ✅ | ✅ |
"warn-error" |
❌ | ✅ | ✅ |
"error-only" |
❌ | ❌ | ✅ |
"none" |
❌ | ❌ | ❌ |
const router = createRouter(routes, {
logger: {
level: "error-only", // Only log errors
callback: (level, context, message) => {
if (level === "error") {
Sentry.captureMessage(`[${context}] ${message}`);
}
},
callbackIgnoresLevel: true, // Callback receives all logs
},
});import { createRouter } from "@real-router/core";
const router = createRouter(routes, {
defaultRoute: "home",
trailingSlash: "never",
allowNotFound: true,
});const router = createRouter(routes, {
defaultRoute: "dashboard",
defaultParams: { view: "overview" },
trailingSlash: "never",
caseSensitive: false,
urlParamsEncoding: "default",
queryParamsMode: "loose",
queryParams: {
arrayFormat: "brackets",
booleanFormat: "auto",
nullFormat: "hidden",
numberFormat: "auto",
},
allowNotFound: true,
rewritePathOnMatch: false,
logger: {
level: "warn-error",
},
});const router = createRouter(routes, {
defaultRoute: "home",
allowNotFound: true,
logger: {
level: "none", // Disable console output
callback: (level, context, message) => {
// Send to monitoring service
if (level === "error") {
errorTracker.capture(`[${context}] ${message}`);
}
},
callbackIgnoresLevel: true,
},
});Resource limits to prevent memory leaks and performance degradation. All limits have sensible defaults; override only when needed.
const router = createRouter(routes, {
limits: {
maxPlugins: 100,
maxDependencies: 200,
},
});| Property | Type | Default | Max | Description |
|---|---|---|---|---|
maxPlugins |
number |
50 |
1000 |
Maximum registered plugins |
maxDependencies |
number |
100 |
10000 |
Maximum injected dependencies |
maxListeners |
number |
10000 |
100000 |
Maximum event listeners per event type |
maxEventDepth |
number |
5 |
100 |
Maximum recursive event propagation depth |
maxLifecycleHandlers |
number |
200 |
10000 |
Maximum canActivate/canDeactivate handlers |
Set any limit to 0 to disable the check (unlimited).
Partial configuration is supported — unspecified limits use defaults:
createRouter(routes, {
limits: { maxPlugins: 100 }, // Only override maxPlugins
});This option has been removed. Validation is now opt-in via @real-router/validation-plugin.
See Migration from noValidate for details.
When @real-router/validation-plugin is installed, several cross-field combinations are diagnosed at router construction or at the first runtime use (core has no such checks without the plugin — invalid combinations become silent DX bugs).
| Combination | Diagnosis | Detected |
|---|---|---|
limits.warnListeners > limits.maxListeners (and maxListeners > 0) |
RangeError thrown |
At router.usePlugin(validationPlugin()) / validateOptions() call |
defaultRoute: "<name>" pointing to a route absent in the tree |
Plain Error thrown |
At usePlugin() retrospective pass |
defaultRoute: callback returning a route name absent in the tree |
Plain Error thrown |
At runtime inside resolveDefault() — first navigateToDefault() / start() fallback |
logger.callbackIgnoresLevel: true with no logger.callback
|
logger.error message |
At validateOptions() call; non-throwing — the option would be ignored |
Without the plugin these cases produce no warning at construction.
{ allowNotFound: false, defaultRoute: "" } is no longer a dead-end configuration: strict-mode unmatched URLs consistently raise ROUTE_NOT_FOUND via onTransitionError (popstate/navigate events) or Promise rejection (start()), regardless of whether defaultRoute is set. See allowNotFound for the full error contract.
import { getPluginApi } from "@real-router/core/api";
// Get all options (returns a copy)
const options = getPluginApi(router).getOptions();Note: Options are immutable after
createRouter(). All settings must be provided at creation time. FordefaultRouteanddefaultParams, callback functions allow dynamic behavior without mutating the options — the function reference is the option, and it is resolved at the point of use.
| Method | Description |
|---|---|
getOptions() |
Get current router options |
createRouter() |
Create router with options |