validation plugin - greydragon888/real-router GitHub Wiki
@real-router/validation-plugin
1. Overview
- Name: Validation Plugin
- Package:
@real-router/validation-plugin - Purpose: Opt-in runtime validation for all router operations. Adds descriptive error messages, argument shape checks, and structural consistency verification.
- Typical scenarios: Development builds where descriptive errors catch mistakes early; CI/test environments; any setup where runtime DX matters more than bundle size.
Clarification — two different kinds of validation:
Plugin What it validates When it runs @real-router/validation-pluginRouter API call arguments — guards that navigate()receives a string,buildPath()receives valid params, etc. Structural consistency of the route tree.Development only — skip in production builds @real-router/search-schema-pluginURL search parameter content — validates the values at state.paramsagainst a Standard Schema V1 object on each route config. Strips or repairs invalid params.Both development and production Use both together:
validation-plugincatches developer mistakes at call sites;search-schema-pluginhandles malformed URLs from real users at runtime.
2. Installation and Setup
npm install @real-router/validation-plugin
# or
pnpm add @real-router/validation-plugin
import { createRouter } from "@real-router/core";
import { validationPlugin } from "@real-router/validation-plugin";
const router = createRouter(routes);
router.usePlugin(validationPlugin()); // register BEFORE start()
await router.start("/");
For production, skip the plugin:
const router = createRouter(routes);
if (process.env.NODE_ENV !== "production") {
router.usePlugin(validationPlugin());
}
await router.start("/");
3. What It Validates
| Namespace | Validated operations |
|---|---|
| Routes | buildPath, matchPath, isActiveRoute, shouldUpdateNode, addRoute, removeRoute, updateRoute, forwardTo targets and cycles |
| Options | limits object shape, individual limit values; cross-field warnListeners ≤ maxListeners; logger.callbackIgnoresLevel requires logger.callback; defaultRoute callback return value |
| Dependencies | setDependency args, dependency name format, full store structure |
| Plugins | Plugin count vs maxPlugins limit, addInterceptor args (method enum + function type) |
| Lifecycle | Guard/hook handler type, count vs maxLifecycleHandlers |
| Navigation | navigate args, navigateToDefault args, NavigationOptions shape, params validation (navigate, buildPath, canNavigateTo), start path validation |
| State | makeState args, areStatesEqual args |
| Event bus | Event name format, listener args |
| Retrospective | Existing route tree integrity, forwardTo consistency, decoder/encoder types, dependency store structure, limits consistency, static defaultRoute resolves to an existing route |
4. Error Message Format
All error messages follow a consistent format:
[router.METHOD] descriptive message, got ${typeDescription}
Examples:
[router.navigate] params must be a plain object, got string
[router.buildPath] route must be a non-empty string, got undefined
[router.addEventListener] Invalid event name: "badEvent". Must be one of: $start, $stop, $$start, $$cancel, $$success, $$error
[router.updateRoute] Route "nonexistent" does not exist
Error Types
| Error type | Semantics |
|---|---|
TypeError |
Wrong argument type or structure |
ReferenceError |
Resource not found (route, dependency) |
RangeError |
Limit exceeded (maxPlugins, maxLifecycleHandlers, maxDependencies) |
Retrospective validation errors use [validation-plugin] prefix instead of [router.METHOD].
5. Retrospective Validation
When you register the plugin, it immediately validates the current state of the router:
const router = createRouter([
{ name: "home", path: "/" },
{ name: "home", path: "/duplicate" }, // duplicate name
]);
router.usePlugin(validationPlugin()); // throws here — duplicate route caught
The retrospective pass validates:
- Existing route tree integrity (duplicates, structure)
forwardTotarget existence and param compatibility- Decoder/encoder types (must be sync functions)
- Dependency store structure
- Limits consistency
- Static
defaultRoutevalue resolves to a registered route - Cross-field
Optionsconstraints (see Cross-Field Constraints)
DefaultRouteCallback is not invoked at registration time — its return value depends on dependencies that may not be set yet. Instead, the plugin installs a runtime hook (RouterValidator.options.validateResolvedDefaultRoute) that core calls from resolveDefault() — so a callback returning a non-existent route name surfaces on the first navigateToDefault() / start() fallback with an actionable error message, not a generic ROUTE_NOT_FOUND.
If the pass fails, the plugin rolls back cleanly (ctx.validator = null), and the error propagates to your code.
6. Core Invariant Guards
Even without this plugin, core has two invariant guards that always run:
subscribe(listener)— throwsTypeErrorif listener is not a function. Includes hint: "For Observable pattern use @real-router/rx package".navigateToNotFound(path)— throwsTypeErrorif path is provided but not a string. Prevents silent state corruption (state.path = 42).
Core also has structural guards for the constructor (route structure, dependency object, options shape) and plugin registration. These are not DX validation — they prevent the router from entering an invalid internal state.
No argument validation exists for navigation methods (navigate, buildPath, start, etc.) without this plugin. Invalid arguments will produce native JavaScript errors.
7. Migration from noValidate
The noValidate option has been removed from RouterOptions. It does not exist in current versions.
Before:
// Old: validation was on by default, noValidate disabled it
const router = createRouter(routes, { noValidate: true });
After:
// New: validation is off by default, plugin enables it
const router = createRouter(routes);
router.usePlugin(validationPlugin()); // opt in
8. API
validationPlugin()
function validationPlugin(): PluginFactory;
Returns a PluginFactory to pass to router.usePlugin(). Takes no arguments.
Throws RouterError("VALIDATION_PLUGIN_AFTER_START") if the router is already active. Always register before router.start().
RouterValidator type
import type { RouterValidator } from "@real-router/validation-plugin";
The full validator interface that core calls into. Namespaced by concern: routes, navigation, state, lifecycle, dependencies, plugins, options, eventBus. Useful for building custom validators or testing.
See Also
- RouterOptions — Router configuration
- Plugin Architecture — How plugins work
- search-schema-plugin — Runtime URL search param validation (runs in production)
- Breaking Changes — Migration notes