Skip to content

the-cookbook/pathkit

Repository files navigation

@cookbook/pathkit

npm version npm downloads Bundle size CI

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

Table of Contents


Installation

pnpm add @cookbook/pathkit
npm install @cookbook/pathkit
yarn add @cookbook/pathkit

Inspiration

@cookbook/pathkit is heavily inspired by the Microsoft ASP.NET route template syntax and route constraint system.

Reference:

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.


Comparison with path-to-regexp

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

Features

  • 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

Route Syntax

Named Parameters

/users/{id}

Optional Parameters

/users/{id?}

Wildcard Parameters

/files/{*path}

Optional Wildcards

/files/{*path?}

Constraints

/users/{id:int}
/posts/{slug:regex([a-z0-9-]+)}
/search/{type:list(view|expanded|details)}

Multiple Constraints

/users/{id:int:range(1,100)}

API

compile()

Compiles a route pattern into a function.

Signature

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;

Example

import { compile } from '@cookbook/pathkit';

const toUser = compile('/users/{id}');

toUser({ id: 10 });

// /users/10

Optional Parameters

const toSearch = compile('/search/{term?}');

toSearch();

// /search

toSearch({ term: 'hello' });

// /search/hello

Wildcards

const toFile = compile('/files/{*path}');

toFile({
  path: ['users', 'john', 'avatar.png'],
});

// /files/users/john/avatar.png

Constraints

const toPage = compile('/page/{type:list(home|dashboard)}');

toPage({ type: 'home' });

// /page/home

Invalid values throw:

toPage({ type: 'settings' });

// Error:
// Parameter "type" must be one of: home, dashboard

Compile Options

delimiter

Changes the route segment delimiter used for wildcard joins and route normalization.

compile('namespace.{*path}', {
  delimiter: '.',
})({
  path: ['frontend', 'typescript', 'routing'],
});

// namespace.frontend.typescript.routing

This is useful for non-slash route styles such as:

  • dot-separated namespaces
  • event routing
  • CLI command patterns
  • message topics
  • internal identifiers

prune

Controls route cleanup behavior after compilation.

Available values:

'all';
'duplication';
'trailing';
false;

'all'

Removes duplicated delimiters and trailing delimiters.

compile('/hello//world/', {
  prune: 'all',
})();

// /hello/world

'duplication'

Removes only duplicated delimiters.

compile('/hello//world/', {
  prune: 'duplication',
})();

// /hello/world/

'trailing'

Removes only trailing delimiters.

compile('/hello//world/', {
  prune: 'trailing',
})();

// /hello//world

false

Disables all cleanup behavior.

compile('/hello//world/', {
  prune: false,
})();

// /hello//world/

match()

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.

Signature

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;

Example

import { match } from '@cookbook/pathkit';

const matcher = match('/users/{id:int}');

matcher('/users/42');

Returns:

{
  match: true,
  params: {
    id: '42',
  },
}

Failed Match

matcher('/users/abc');

Returns:

{
  match: false,
  params: null,
}

Strict Match

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.


Optional Parameters

const matcher = match('/search/{term?}');

matcher('/search');

Returns:

{
  match: true,
  params: {},
}

Wildcards

const matcher = match('/files/{*path}');

matcher('/files/users/john/avatar.png');

Returns:

{
  match: true,
  params: {
    path: 'users/john/avatar.png',
  },
}

Match Options

delimiter

Supports non-slash route styles.

const matcher = match('.users.{id}', {
  delimiter: '.',
});

matcher('.users.10');

trailing

Controls trailing delimiter matching.

match('/hello/{name}', {
  trailing: false,
});

strict

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'

tokenize()

Tokenizes a route pattern into route segments.

Signature

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[];

Example

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: '',
      },
    ],
  },
];

validateRoute()

Validates route patterns before runtime usage.

Signature

declare const validateRoute: (route: string) => void;

Example

import { validateRoute } from '@cookbook/pathkit';

validateRoute('/users/{id:int}');

Invalid routes throw descriptive errors.

validateRoute('/users/{id:unknown}');

// Error:
// [Constraint]: Unknown constraint type: "unknown"

Built-in Constraints

Constraints validate parameter values during compile() and match().

Each constraint can also provide:

  • verify() to validate the route constraint configuration itself
  • toRegExp() to generate the matching pattern used by match()

ConstraintValidation API

interface ConstraintValidation {
  (paramName: string, value: string | number | boolean | undefined, params: string): void;

  verify(paramName: string, params: string): void;

  toRegExp(params: string): string;
}

int

Validates that a parameter is an integer.

Syntax

{id:int}

Example

/users/{id:int}

Valid

/users/1
/users/42
/users/9000

Invalid

/users/abc
/users/1.5
/users/foo-1

Notes

  • Does not accept constraint parameters
  • Uses \d+ as its match pattern
  • Runtime validation is also applied during compile() and during match() when a path candidate matches the generated pattern

range

Validates that a numeric parameter is inside an inclusive range.

Syntax

{id:range(min,max)}

Example

/users/{id:range(1,100)}

Valid

/users/1
/users/50
/users/100

Invalid

/users/0
/users/101
/users/abc

Notes

  • min and max are required
  • The range is inclusive
  • Values are validated numerically

list

Validates that a parameter matches one item from a pipe-separated list.

Syntax

{param:list(item1|item2|item3)}

Example

/search/{type:list(view|expanded|details)}

Valid

/search/view
/search/expanded
/search/details

Invalid

/search/grid
/search/detail

Notes

  • Items are separated with |
  • Matching is exact
  • List values are also used to generate the matcher RegExp

regex

Validates that a parameter matches a custom regular expression.

Syntax

{param:regex(pattern)}

Example

/posts/{slug:regex([a-z0-9-]+)}

Valid

/posts/hello-world
/posts/post-123

Invalid

/posts/HelloWorld
/posts/hello_world

Notes

  • The regex is used by both compile() validation and match() 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

Custom constraints are registered globally at runtime.

A custom constraint must be created using createConstraint.

createConstraint

Creates a custom parameter constraint implementation.

Signature

declare const createConstraint = ({
  parse,
  verify,
  toRegExp,
}: {
  parse: (...args: Parameters<ConstraintValidation>) => void;
  verify: ConstraintValidation['verify'];
  toRegExp: ConstraintValidation['toRegExp'];
}) => ConstraintValidation;

Methods

parse

Implements the runtime validation logic for the parameter value.

This method is executed when the route parameter is matched and receives:

  • paramName: parameter name
  • value: extracted parameter value
  • params: constraint configuration value

Throw an error if the parameter value is invalid.

verify

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

toRegExp

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.

Example

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.


registerConstraint()

Registers or replaces a constraint.

Signature

declare const registerConstraint: (name: string, constraint: ConstraintValidation) => void;

If a constraint with the same name already exists, it is replaced.

Example

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 slug

unregisterConstraint()

Removes a runtime constraint.

Signature

declare const unregisterConstraint: (name: string) => void;

Example

import { unregisterConstraint } from '@cookbook/pathkit';

unregisterConstraint('slug');

hasConstraint()

Checks whether a constraint exists.

Signature

declare const hasConstraint: (name: string) => boolean;

Example

import { hasConstraint } from '@cookbook/pathkit';

hasConstraint('slug');

getConstraint()

Returns a registered constraint.

Signature

declare const getConstraint: (name: string) => ConstraintValidation | undefined;

Example

import { getConstraint } from '@cookbook/pathkit';

const constraint = getConstraint('slug');

resetConstraints()

Restores the built-in constraint registry and removes runtime customizations.

Useful for tests.

Signature

declare const resetConstraints: () => void;

TypeScript

Route Segments

import type { RouteSegment, LiteralSegment, ParameterSegment } from '@cookbook/pathkit';

Constraints

import type { Constraint, ConstraintValidation } from '@cookbook/pathkit';

Match Results

import type { MatchedParam } from '@cookbook/pathkit';

Module Imports

Root Import

import { compile, match, tokenize, validateRoute } from '@cookbook/pathkit';

Constraint Namespace

import { constraints } from '@cookbook/pathkit';

constraints.registerConstraint(...);

Deep Imports

import match from '@cookbook/pathkit/match';
import compile from '@cookbook/pathkit/compile';

Error Handling

All validation and parsing errors use standard Error instances with descriptive messages.

compile()

compile() throws when required params are missing or provided params do not satisfy constraints.

[Compile] Missing required parameter: id
Parameter "page" must be one of: home, dashboard

match()

match() 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'

tokenize() / validateRoute()

Invalid route patterns and invalid constraint declarations throw.

[Tokenize] Invalid route pattern: Unexpected token
[Constraint]: Unknown constraint type: "unknown"

Examples

See the examples directory for complete real-world usage examples.


Design Goals

  • Predictable behavior
  • Minimal abstractions
  • Runtime safety
  • Composable APIs
  • Framework independence
  • Extensibility through constraints
  • Small API surface

License

MIT