A lightweight route compiler, matcher, tokenizer, and validation toolkit for JavaScript and TypeScript.
@cookbook/pathkit provides a predictable and extensible route pattern system with support for:
- Route compilation
- Route matching
- Route tokenization
- Route validation
- Optional parameters
- Wildcard parameters
- Runtime constraints
- Custom constraints
- Custom delimiters
- Parameter type enforcement
- Strict match validation
- TypeScript support
- ESM and CommonJS
- Installation
- Inspiration
- Comparison with
path-to-regexp - Features
- Route Syntax
- API
- Built-in Constraints
- Custom Constraints
- TypeScript
- Module Imports
- Error Handling
- Examples
- Design Goals
- License
pnpm add @cookbook/pathkitnpm install @cookbook/pathkityarn add @cookbook/pathkit@cookbook/pathkit is heavily inspired by the Microsoft ASP.NET route template syntax and route constraint system.
Reference:
- ASP.NET Core Route Constraints Documentation https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-9.0#route-constraints
Examples:
/users/{id}
/users/{id:int}
/files/{*path}
/posts/{slug:regex([a-z0-9-]+)}The goal is to provide a powerful and expressive route syntax for JavaScript and TypeScript applications while keeping the implementation lightweight and framework agnostic.
| Feature | @cookbook/pathkit | path-to-regexp |
|---|---|---|
| Route compilation | Yes | Yes |
| Route matching | Yes | Yes |
| Route tokenization | Yes | Partial |
| Route validation | Yes | No |
| Runtime constraint system | Yes | No |
| Built-in constraints | Yes | No |
| Custom constraints | Yes | Limited/custom parsing required |
| Optional parameters | Yes | Yes |
| Wildcard parameters | Yes | Yes |
| Parameter type enforcement | Yes | No |
| Strict match validation | Yes | No |
| TypeScript-first API | Yes | Partial |
| Framework agnostic | Yes | Yes |
| Zero dependencies | Yes | No |
| Runtime-safe constraint validation | Yes | No |
path-to-regexp focuses primarily on transforming path patterns into regular expressions.
@cookbook/pathkit focuses on complete route tooling:
- Route parsing
- Validation
- Runtime-safe constraints
- Typed route segments
- Route compilation
- Route matching
- Extensibility through runtime constraint registration
- Zero dependencies
- Small runtime footprint
- Runtime-safe route validation
- Extensible constraint registry
- Functional API
- Framework agnostic
- SSR compatible
- ESM + CommonJS exports
- Strong TypeScript support
- Optional strict matching for debugging constraint failures
/users/{id}/users/{id?}/files/{*path}/files/{*path?}/users/{id:int}
/posts/{slug:regex([a-z0-9-]+)}
/search/{type:list(view|expanded|details)}/users/{id:int:range(1,100)}Compiles a route pattern into a function.
interface CompileOptions {
delimiter?: string;
prune?: 'all' | 'duplication' | 'trailing' | false;
}
type TypeOrArray<T> = T | T[];
interface CompileParams {
[key: string]: TypeOrArray<string | number | boolean> | null | undefined;
}
declare const compile: (
route: string,
options?: CompileOptions,
) => (params?: CompileParams) => string;import { compile } from '@cookbook/pathkit';
const toUser = compile('/users/{id}');
toUser({ id: 10 });
// /users/10const toSearch = compile('/search/{term?}');
toSearch();
// /search
toSearch({ term: 'hello' });
// /search/helloconst toFile = compile('/files/{*path}');
toFile({
path: ['users', 'john', 'avatar.png'],
});
// /files/users/john/avatar.pngconst toPage = compile('/page/{type:list(home|dashboard)}');
toPage({ type: 'home' });
// /page/homeInvalid values throw:
toPage({ type: 'settings' });
// Error:
// Parameter "type" must be one of: home, dashboardChanges the route segment delimiter used for wildcard joins and route normalization.
compile('namespace.{*path}', {
delimiter: '.',
})({
path: ['frontend', 'typescript', 'routing'],
});
// namespace.frontend.typescript.routingThis is useful for non-slash route styles such as:
- dot-separated namespaces
- event routing
- CLI command patterns
- message topics
- internal identifiers
Controls route cleanup behavior after compilation.
Available values:
'all';
'duplication';
'trailing';
false;Removes duplicated delimiters and trailing delimiters.
compile('/hello//world/', {
prune: 'all',
})();
// /hello/worldRemoves only duplicated delimiters.
compile('/hello//world/', {
prune: 'duplication',
})();
// /hello/world/Removes only trailing delimiters.
compile('/hello//world/', {
prune: 'trailing',
})();
// /hello//worldDisables all cleanup behavior.
compile('/hello//world/', {
prune: false,
})();
// /hello//world/Matches a route pattern against a path.
By default, match() is router-safe: constraint validation failures return a failed match instead of throwing. This makes it suitable for trying multiple route candidates.
Use strict: true when you want constraint validation errors to be thrown for debugging or development tooling.
interface MatchOptions {
delimiter?: string;
trailing?: boolean;
strict?: boolean;
}
type MatchedParam = Record<string, string | string[] | null | undefined>;
interface MatchResult {
match: boolean;
params: MatchedParam | null;
}
declare const match: (route: string, options?: MatchOptions) => (path: string) => MatchResult;import { match } from '@cookbook/pathkit';
const matcher = match('/users/{id:int}');
matcher('/users/42');Returns:
{
match: true,
params: {
id: '42',
},
}matcher('/users/abc');Returns:
{
match: false,
params: null,
}By default, invalid constrained values return a failed match:
const matcher = match('/users/{id:int}');
matcher('/users/abc');Returns:
{
match: false,
params: null,
}Enable strict mode to throw constraint validation errors:
const strictMatcher = match('/users/{id:int}', {
strict: true,
});
strictMatcher('/users/abc');Throws:
[Constraint] Parameter "id" must be a number, instead got 'string'This is useful for development tools, tests, debugging, and cases where an invalid constrained value should be treated as an application error instead of a non-match.
const matcher = match('/search/{term?}');
matcher('/search');Returns:
{
match: true,
params: {},
}const matcher = match('/files/{*path}');
matcher('/files/users/john/avatar.png');Returns:
{
match: true,
params: {
path: 'users/john/avatar.png',
},
}Supports non-slash route styles.
const matcher = match('.users.{id}', {
delimiter: '.',
});
matcher('.users.10');Controls trailing delimiter matching.
match('/hello/{name}', {
trailing: false,
});Controls whether constraint validation errors are thrown.
Default:
strict: false;When strict is disabled, constraint validation failures return:
{
match: false,
params: null,
}When strict is enabled, constraint validation failures are thrown:
match('/users/{id:int}', {
strict: true,
})('/users/abc');Throws:
[Constraint] Parameter "id" must be a number, instead got 'string'Tokenizes a route pattern into route segments.
type TokenType = 'literal' | 'parameter';
interface Constraint {
type: string;
params: string;
}
interface LiteralSegment {
type: 'literal';
value: string;
}
interface ParameterSegment {
type: 'parameter';
name: string;
wildcard: boolean;
optional: boolean;
constraints: Constraint[];
}
type RouteSegment = LiteralSegment | ParameterSegment;
declare const tokenize: (route: string) => RouteSegment[];import { tokenize } from '@cookbook/pathkit';
tokenize('/users/{id:int}');Returns:
[
{
type: 'literal',
value: '/users/',
},
{
type: 'parameter',
name: 'id',
wildcard: false,
optional: false,
constraints: [
{
type: 'int',
params: '',
},
],
},
];Validates route patterns before runtime usage.
declare const validateRoute: (route: string) => void;import { validateRoute } from '@cookbook/pathkit';
validateRoute('/users/{id:int}');Invalid routes throw descriptive errors.
validateRoute('/users/{id:unknown}');
// Error:
// [Constraint]: Unknown constraint type: "unknown"Constraints validate parameter values during compile() and match().
Each constraint can also provide:
verify()to validate the route constraint configuration itselftoRegExp()to generate the matching pattern used bymatch()
interface ConstraintValidation {
(paramName: string, value: string | number | boolean | undefined, params: string): void;
verify(paramName: string, params: string): void;
toRegExp(params: string): string;
}Validates that a parameter is an integer.
{id:int}/users/{id:int}/users/1
/users/42
/users/9000/users/abc
/users/1.5
/users/foo-1- Does not accept constraint parameters
- Uses
\d+as its match pattern - Runtime validation is also applied during
compile()and duringmatch()when a path candidate matches the generated pattern
Validates that a numeric parameter is inside an inclusive range.
{id:range(min,max)}/users/{id:range(1,100)}/users/1
/users/50
/users/100/users/0
/users/101
/users/abcminandmaxare required- The range is inclusive
- Values are validated numerically
Validates that a parameter matches one item from a pipe-separated list.
{param:list(item1|item2|item3)}/search/{type:list(view|expanded|details)}/search/view
/search/expanded
/search/details/search/grid
/search/detail- Items are separated with
| - Matching is exact
- List values are also used to generate the matcher RegExp
Validates that a parameter matches a custom regular expression.
{param:regex(pattern)}/posts/{slug:regex([a-z0-9-]+)}/posts/hello-world
/posts/post-123/posts/HelloWorld
/posts/hello_world- The regex is used by both
compile()validation andmatch()route matching - Do not include route delimiters unless the parameter is intended to match them
- For cross-segment matching, use a wildcard parameter instead
Custom constraints are registered globally at runtime.
A custom constraint must be created using createConstraint.
Creates a custom parameter constraint implementation.
declare const createConstraint = ({
parse,
verify,
toRegExp,
}: {
parse: (...args: Parameters<ConstraintValidation>) => void;
verify: ConstraintValidation['verify'];
toRegExp: ConstraintValidation['toRegExp'];
}) => ConstraintValidation;Implements the runtime validation logic for the parameter value.
This method is executed when the route parameter is matched and receives:
paramName: parameter namevalue: extracted parameter valueparams: constraint configuration value
Throw an error if the parameter value is invalid.
Validates the constraint configuration itself.
Use this method to ensure the constraint declaration is valid and correctly formatted before parse is executed.
Typical use cases include:
- validating constraint arguments
- rejecting unsupported parameters
- validating parameter formatting
Returns the regular expression pattern used to extract and match the parameter value from the route.
The returned value must be a valid regex pattern string without delimiters.
import { createConstraint } from '@cookbook/pathkit';
const slug = createConstraint({
parse: (paramName, value) => {
if (typeof value !== 'string') {
throw new Error(`Parameter "${paramName}" must be a string`);
}
if (!/^[a-z0-9-]+$/.test(value)) {
throw new Error(`Parameter "${paramName}" must be a valid slug`);
}
},
verify: (paramName, params) => {
if (params.trim().length) {
throw new Error(
`[Constraint] Constraint 'slug' declared for '${paramName}' does not accept parameters, ` +
`but received '${params}'.`,
);
}
},
toRegExp: () => '[a-z0-9-]+',
});Note: verify is called automatically before parse is executed.
Registers or replaces a constraint.
declare const registerConstraint: (name: string, constraint: ConstraintValidation) => void;If a constraint with the same name already exists, it is replaced.
import { match, registerConstraint } from '@cookbook/pathkit';
registerConstraint('slug', slug);
const matcher = match('/posts/{slug:slug}');
matcher('/posts/hello-world');Returns:
{
match: true,
params: {
slug: 'hello-world',
},
}Invalid values return a failed match by default:
matcher('/posts/heiß');Returns:
{
match: false,
params: null,
}Use strict mode to throw the custom constraint error:
const strictMatcher = match('/posts/{slug:slug}', {
strict: true,
});
strictMatcher('/posts/heiß');Throws:
Parameter "slug" must be a valid slugRemoves a runtime constraint.
declare const unregisterConstraint: (name: string) => void;import { unregisterConstraint } from '@cookbook/pathkit';
unregisterConstraint('slug');Checks whether a constraint exists.
declare const hasConstraint: (name: string) => boolean;import { hasConstraint } from '@cookbook/pathkit';
hasConstraint('slug');Returns a registered constraint.
declare const getConstraint: (name: string) => ConstraintValidation | undefined;import { getConstraint } from '@cookbook/pathkit';
const constraint = getConstraint('slug');Restores the built-in constraint registry and removes runtime customizations.
Useful for tests.
declare const resetConstraints: () => void;import type { RouteSegment, LiteralSegment, ParameterSegment } from '@cookbook/pathkit';import type { Constraint, ConstraintValidation } from '@cookbook/pathkit';import type { MatchedParam } from '@cookbook/pathkit';import { compile, match, tokenize, validateRoute } from '@cookbook/pathkit';import { constraints } from '@cookbook/pathkit';
constraints.registerConstraint(...);import match from '@cookbook/pathkit/match';
import compile from '@cookbook/pathkit/compile';All validation and parsing errors use standard Error instances with descriptive messages.
compile() throws when required params are missing or provided params do not satisfy constraints.
[Compile] Missing required parameter: idParameter "page" must be one of: home, dashboardmatch() returns failed matches by default when a path does not match the route or does not satisfy route constraints.
{
match: false,
params: null,
}With strict: true, constraint validation errors are thrown instead of being converted into failed matches.
[Constraint] Parameter "id" must be a number, instead got 'string'Invalid route patterns and invalid constraint declarations throw.
[Tokenize] Invalid route pattern: Unexpected token[Constraint]: Unknown constraint type: "unknown"See the examples directory for complete real-world usage examples.
- Predictable behavior
- Minimal abstractions
- Runtime safety
- Composable APIs
- Framework independence
- Extensibility through constraints
- Small API surface
MIT