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-plugin Router 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-plugin URL search parameter content — validates the values at state.params against a Standard Schema V1 object on each route config. Strips or repairs invalid params. Both development and production

Use both together: validation-plugin catches developer mistakes at call sites; search-schema-plugin handles 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)
  • forwardTo target existence and param compatibility
  • Decoder/encoder types (must be sync functions)
  • Dependency store structure
  • Limits consistency
  • Static defaultRoute value resolves to a registered route
  • Cross-field Options constraints (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) — throws TypeError if listener is not a function. Includes hint: "For Observable pattern use @real-router/rx package".
  • navigateToNotFound(path) — throws TypeError if 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