RouterOptions - greydragon888/real-router GitHub Wiki

RouterOptions

Overview

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.

Type Definition

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;

Properties

defaultRoute

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() and navigateToDefault() call. getOptions().defaultRoute returns the function reference, not the resolved value.

defaultParams

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" }

trailingSlash

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 /users

"preserve" + rewritePathOnMatch: true

When 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 added

Rewrite semantics (forwardTo, encoders, defaultParams merge) still apply — only trailing-slash behaviour is respected. This preserves the contract promised by "preserve".

caseSensitive

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

urlParamsEncoding

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

queryParamsMode

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

queryParams

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";
}

arrayFormat

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"] }

booleanFormat

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 }

nullFormat

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.

numberFormat

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.

Params Contract (undefined-strip)

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" absent

null 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.

allowNotFound

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 error

When 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 with ROUTE_NOT_FOUND.
  • Popstate events emit $$error (reaching the onTransitionError plugin hook) and roll the browser URL back to the last-known router state. Router state is unchanged.
  • Navigation API navigate events do the same, with URL rollback handled automatically by event.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 });
    }
  },
}));

rewritePathOnMatch

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).

logger

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;
}

Logger Level Thresholds

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
  },
});

Usage Examples

Basic Configuration

import { createRouter } from "@real-router/core";

const router = createRouter(routes, {
  defaultRoute: "home",
  trailingSlash: "never",
  allowNotFound: true,
});

Full Configuration

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",
  },
});

Production Configuration

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,
  },
});

limits

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,
  },
});

LimitsConfig

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
});

noValidate (removed)

This option has been removed. Validation is now opt-in via @real-router/validation-plugin.

See Migration from noValidate for details.


Cross-Field Constraints

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.


Reading Options

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. For defaultRoute and defaultParams, callback functions allow dynamic behavior without mutating the options — the function reference is the option, and it is resolved at the point of use.

Related Methods

Method Description
getOptions() Get current router options
createRouter() Create router with options
⚠️ **GitHub.com Fallback** ⚠️