An open API service indexing awesome lists of open source software.

https://github.com/the-cookbook/pathkit

A lightweight route pattern parser, matcher and compiler for TypeScript.
https://github.com/the-cookbook/pathkit

developer-tools javascript node params path path-to-regexp route-compiler route-constraints route-match route-parser router routing typescript url-generator url-routing validation zero-dependencies

Last synced: 14 days ago
JSON representation

A lightweight route pattern parser, matcher and compiler for TypeScript.

Awesome Lists containing this project

README

          

# `@cookbook/pathkit`

[![npm version](https://img.shields.io/npm/v/@cookbook/pathkit.svg)](https://www.npmjs.com/package/@cookbook/pathkit)
[![npm downloads](https://img.shields.io/npm/dm/@cookbook/pathkit.svg)](https://www.npmjs.com/package/@cookbook/pathkit)
[![Bundle size](https://img.shields.io/bundlephobia/minzip/@cookbook/pathkit)](https://bundlephobia.com/package/@cookbook/pathkit)
[![CI](https://github.com/the-cookbook/pathkit/actions/workflows/ci.yml/badge.svg)](https://github.com/the-cookbook/pathkit/actions/workflows/ci.yml)

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](#installation)
- [Inspiration](#inspiration)
- [Comparison with `path-to-regexp`](#comparison-with-path-to-regexp)
- [Features](#features)
- [Route Syntax](#route-syntax)
- [Named Parameters](#named-parameters)
- [Optional Parameters](#optional-parameters)
- [Wildcard Parameters](#wildcard-parameters)
- [Optional Wildcards](#optional-wildcards)
- [Constraints](#constraints)
- [Multiple Constraints](#multiple-constraints)
- [API](#api)
- [compile()](#compile)
- [Signature](#signature)
- [Example](#example)
- [Optional Parameters](#optional-parameters-1)
- [Wildcards](#wildcards)
- [Constraints](#constraints-1)
- [Compile Options](#compile-options)
- [delimiter](#delimiter)
- [prune](#prune)
- [match()](#match)
- [Signature](#signature-1)
- [Example](#example-1)
- [Failed Match](#failed-match)
- [Strict Match](#strict-match)
- [Optional Parameters](#optional-parameters-2)
- [Wildcards](#wildcards-1)
- [Match Options](#match-options)
- [delimiter](#delimiter-1)
- [trailing](#trailing)
- [strict](#strict)
- [tokenize()](#tokenize)
- [Signature](#signature-2)
- [Example](#example-2)
- [validateRoute()](#validateroute)
- [Signature](#signature-3)
- [Example](#example-3)
- [Built-in Constraints](#built-in-constraints)
- [ConstraintValidation API](#constraintvalidation-api)
- [decimal](#decimal)
- [int](#int)
- [range](#range)
- [list](#list)
- [regex](#regex)
- [Custom Constraints](#custom-constraints)
- [createConstraint](#createConstraint)
- [registerConstraint()](#registerconstraint)
- [unregisterConstraint()](#unregisterconstraint)
- [hasConstraint()](#hasconstraint)
- [getConstraint()](#getconstraint)
- [resetConstraints()](#resetconstraints)
- [TypeScript](#typescript)
- [Route Segments](#route-segments)
- [Constraints](#constraints-2)
- [Match Results](#match-results)
- [Module Imports](#module-imports)
- [Root Import](#root-import)
- [Constraint Namespace](#constraint-namespace)
- [Deep Imports](#deep-imports)
- [Error Handling](#error-handling)
- [Examples](#examples)
- [Design Goals](#design-goals)
- [License](#license)

---

# Installation

```bash
pnpm add @cookbook/pathkit
```

```bash
npm install @cookbook/pathkit
```

```bash
yarn add @cookbook/pathkit
```

---

# Inspiration

`@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](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-9.0#route-constraints)

Examples:

```txt
/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

```txt
/users/{id}
```

## Optional Parameters

```txt
/users/{id?}
```

## Wildcard Parameters

```txt
/files/{*path}
```

## Optional Wildcards

```txt
/files/{*path?}
```

## Constraints

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

## Multiple Constraints

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

---

# API

# compile()

Compiles a route pattern into a function.

## Signature

```ts
interface CompileOptions {
delimiter?: string;
prune?: 'all' | 'duplication' | 'trailing' | false;
}

type TypeOrArray = T | T[];

interface CompileParams {
[key: string]: TypeOrArray | null | undefined;
}

declare const compile: (
route: string,
options?: CompileOptions,
) => (params?: CompileParams) => string;
```

## Example

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

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

toUser({ id: 10 });

// /users/10
```

---

## Optional Parameters

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

toSearch();

// /search

toSearch({ term: 'hello' });

// /search/hello
```

---

## Wildcards

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

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

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

---

## Constraints

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

toPage({ type: 'home' });

// /page/home
```

Invalid values throw:

```ts
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.

```ts
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:

```ts
'all';
'duplication';
'trailing';
false;
```

### `'all'`

Removes duplicated delimiters and trailing delimiters.

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

// /hello/world
```

---

### `'duplication'`

Removes only duplicated delimiters.

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

// /hello/world/
```

---

### `'trailing'`

Removes only trailing delimiters.

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

// /hello//world
```

---

### `false`

Disables all cleanup behavior.

```ts
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

```ts
interface MatchOptions {
delimiter?: string;
trailing?: boolean;
strict?: boolean;
}

type MatchedParam = Record;

interface MatchResult {
match: boolean;
params: MatchedParam | null;
}

declare const match: (route: string, options?: MatchOptions) => (path: string) => MatchResult;
```

## Example

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

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

matcher('/users/42');
```

Returns:

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

---

## Failed Match

```ts
matcher('/users/abc');
```

Returns:

```ts
{
match: false,
params: null,
}
```

---

## Strict Match

By default, invalid constrained values return a failed match:

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

matcher('/users/abc');
```

Returns:

```ts
{
match: false,
params: null,
}
```

Enable `strict` mode to throw constraint validation errors:

```ts
const strictMatcher = match('/users/{id:int}', {
strict: true,
});

strictMatcher('/users/abc');
```

Throws:

```txt
[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

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

matcher('/search');
```

Returns:

```ts
{
match: true,
params: {},
}
```

---

## Wildcards

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

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

Returns:

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

---

# Match Options

## delimiter

Supports non-slash route styles.

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

matcher('.users.10');
```

---

## trailing

Controls trailing delimiter matching.

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

---

## strict

Controls whether constraint validation errors are thrown.

Default:

```ts
strict: false;
```

When `strict` is disabled, constraint validation failures return:

```ts
{
match: false,
params: null,
}
```

When `strict` is enabled, constraint validation failures are thrown:

```ts
match('/users/{id:int}', {
strict: true,
})('/users/abc');
```

Throws:

```txt
[Constraint] Parameter "id" must be a number, instead got 'string'
```

---

# tokenize()

Tokenizes a route pattern into route segments.

## Signature

```ts
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

```ts
import { tokenize } from '@cookbook/pathkit';

tokenize('/users/{id:int}');
```

Returns:

```ts
[
{
type: 'literal',
value: '/users/',
},
{
type: 'parameter',
name: 'id',
wildcard: false,
optional: false,
constraints: [
{
type: 'int',
params: '',
},
],
},
];
```

---

# validateRoute()

Validates route patterns before runtime usage.

## Signature

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

## Example

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

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

Invalid routes throw descriptive errors.

```ts
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

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

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

toRegExp(params: string): string;
}
```

---

## `decimal`

Validates that a parameter is a decimal.

### Syntax

```txt
{price:decimal}
```

### Example

```txt
/products/by-price/{price:decimal}
```

### Valid

```txt
/products/1
/products/1.5
/products/42
/products/9000
/products/200.99
```

### Invalid

```txt
/products/abc
/products/foo-1
```

### Notes

- Does not accept constraint parameters

---

## `int`

Validates that a parameter is an integer.

### Syntax

```txt
{id:int}
```

### Example

```txt
/users/{id:int}
```

### Valid

```txt
/users/1
/users/42
/users/9000
```

### Invalid

```txt
/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

```txt
{id:range(min,max)}
```

### Example

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

### Valid

```txt
/users/1
/users/50
/users/100
```

### Invalid

```txt
/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

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

### Example

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

### Valid

```txt
/search/view
/search/expanded
/search/details
```

### Invalid

```txt
/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

```txt
{param:regex(pattern)}
```

### Example

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

### Valid

```txt
/posts/hello-world
/posts/post-123
```

### Invalid

```txt
/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

```ts
declare const createConstraint = ({
parse,
verify,
toRegExp,
}: {
parse: (...args: Parameters) => 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

```ts
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

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

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

### Example

```ts
import { match, registerConstraint } from '@cookbook/pathkit';

registerConstraint('slug', slug);

const matcher = match('/posts/{slug:slug}');

matcher('/posts/hello-world');
```

Returns:

```ts
{
match: true,
params: {
slug: 'hello-world',
},
}
```

Invalid values return a failed match by default:

```ts
matcher('/posts/heiß');
```

Returns:

```ts
{
match: false,
params: null,
}
```

Use strict mode to throw the custom constraint error:

```ts
const strictMatcher = match('/posts/{slug:slug}', {
strict: true,
});

strictMatcher('/posts/heiß');
```

Throws:

```txt
Parameter "slug" must be a valid slug
```

---

## unregisterConstraint()

Removes a runtime constraint.

### Signature

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

### Example

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

unregisterConstraint('slug');
```

---

## hasConstraint()

Checks whether a constraint exists.

### Signature

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

### Example

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

hasConstraint('slug');
```

---

## getConstraint()

Returns a registered constraint.

### Signature

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

### Example

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

const constraint = getConstraint('slug');
```

---

## resetConstraints()

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

Useful for tests.

### Signature

```ts
declare const resetConstraints: () => void;
```

---

# TypeScript

## Route Segments

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

---

## Constraints

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

---

## Match Results

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

---

# Module Imports

## Root Import

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

---

## Constraint Namespace

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

constraints.registerConstraint(...);
```

---

## Deep Imports

```ts
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.

```txt
[Compile] Missing required parameter: id
```

```txt
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.

```ts
{
match: false,
params: null,
}
```

With `strict: true`, constraint validation errors are thrown instead of being converted into failed matches.

```txt
[Constraint] Parameter "id" must be a number, instead got 'string'
```

## tokenize() / validateRoute()

Invalid route patterns and invalid constraint declarations throw.

```txt
[Tokenize] Invalid route pattern: Unexpected token
```

```txt
[Constraint]: Unknown constraint type: "unknown"
```

---

# Examples

See the [`examples`](./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