npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cookbook/urlkit

v2.1.0

Published

Framework-agnostic typed URL contract library for parsing, validating, normalizing, matching, and building URL state.

Readme

@cookbook/urlkit

npm version npm downloads Bundle size license CI

Framework-agnostic URL contracts for parsing, validating, normalizing, matching, and building URL state.

URLKit defines reusable URL contracts once, then uses them to parse, validate, normalize, match, and build path params, search params, hash fragments, requests, hrefs, filters, and other URL-driven application state wherever URLs are consumed.

Without URLKit

const rawUrl = new URL(
  '/articles/42?page=2&tag=ts&tag=urlkit&sort=popular#comments',
  'https://your-app.com',
);

const id = Number(rawUrl.pathname.split('/').at(-1));
const pageValues = rawUrl.searchParams.getAll('page');
const tagValues = rawUrl.searchParams.getAll('tag');
const sortValues = rawUrl.searchParams.getAll('sort');
const hash = rawUrl.hash ? rawUrl.hash.slice(1) : undefined;

if (!Number.isInteger(id)) {
  throw new Error('Invalid article id.');
}

if (pageValues.length > 1) {
  throw new Error('Expected only one page value.');
}

if (sortValues.length > 1) {
  throw new Error('Expected only one sort value.');
}

const page = Number(pageValues[0] ?? '1');
const sort = sortValues[0] ?? 'newest';

if (!Number.isInteger(page)) {
  throw new Error('Invalid page.');
}

if (sort !== 'newest' && sort !== 'popular') {
  throw new Error('Invalid sort.');
}

if (hash !== undefined && hash !== 'comments' && hash !== 'share') {
  throw new Error('Invalid hash.');
}

...

With URLKit

const ArticleUrl = url({
  path: '/articles/{id:int}',
  search: {
    page: int().default(1),
    tag: array(string()).optional(),
    sort: enumOf(['newest', 'popular']).default('newest'),
  },
  hash: enumOf(['comments', 'share']).optional(),
});

const { params, search, hash } = ArticleUrl.parse('/articles/42?page=2&tag=ts&tag=urlkit&sort=popular#comments');

// params.id: number
// search.page: number
// search.tag: string[] | undefined
// search.sort: "newest" | "popular"
// hash: "comments" | "share" | undefined

...

Define the URL shape once. URLKit validates the path, parses params, applies defaults, handles repeated query values, validates the hash, and returns typed URL state you can use right away.

Documentation

Real-world framework examples

Full integration examples are available under examples/integrations. They show the same product catalog contracts used with Next.js, Express, Hono, Fastify, React Router, Remix, and TanStack Router, including local Express/Hono/Fastify middleware wrappers that accept a URLKit contract plus options.

Table of contents

Installation

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

Quick start

import { int, string, url } from '@cookbook/urlkit';

const UserUrl = url({
  path: '/users/{id:int}',
  search: {
    tab: string().default('profile'),
    page: int().default(1),
  },
});

const state = UserUrl.parse('/users/42?tab=settings&page=2');
// state.params.id: number
// state.search.tab: string

const href = UserUrl.build({
  params: { id: 42 },
  search: { tab: 'settings', page: 2 },
});
// '/users/42?tab=settings&page=2'

Why URLKit?

URLs usually cross boundaries as strings, but application code wants typed state. URLKit gives you one reusable contract for:

  • parsing serialized URLs into typed state
  • normalizing structured params/search/hash from framework or server inputs
  • building canonical URLs from typed state
  • validating and matching URLs without routing dependencies
  • sharing the same URL contract across browser, server, edge, router, CLI, and test environments

Package exports

| Import path | Purpose | | --------------------------------- | ---------------------------------------------------------------------------- | | @cookbook/urlkit | Runtime URL contracts, schema builders, public contracts, and UrlKitError. | | @cookbook/urlkit/static | Static descriptor compilers for router-compatible analyzable descriptors. | | @cookbook/urlkit/router-runtime | Framework-agnostic runtime helpers for router packages. |

import { url, search, hash, string, int, enumOf } from '@cookbook/urlkit';
import { compileStaticUrl } from '@cookbook/urlkit/static';
import { createRouteUrlContract } from '@cookbook/urlkit/router-runtime';

Core concepts

parse, normalize, build, and match

| Method | Input | Purpose | | ----------- | --------------------------------------- | ------------------------------------------------------------------------- | | parse | Serialized URL input: string or URL | Parse and validate a URL string/object into typed UrlState. | | normalize | Structured URL state | Validate/coerce params, search, and hash from application/framework data. | | build | Typed URL state | Serialize state to a canonical URL string. | | match | Serialized URL input: string or URL | Return true/false for ordinary URL validation. |

parse intentionally does not accept structured objects. Use normalize for structured state.

UrlState

Parsed and normalized state always includes pathname, params, search, and hash. Optional hashes are represented as undefined; the hash property itself is still present.

interface UrlState<Pathname, Params, Search, Hash> {
  readonly pathname: Pathname;
  readonly params: Params;
  readonly search: Search;
  readonly hash: Hash;
  readonly unknownSearch?: UnknownSearchParams;
}

Preserved unknown search params live in state.unknownSearch, not in state.search.

Path-based vs pathless contracts

| Mode | How to create it | Path behavior | Build behavior | | ---------- | ---------------- | ------------------------------------------- | ------------------------------------------------------------------- | | Path-based | Provide path | Validates pathnames and infers path params. | Builds from params. | | Pathless | Omit path | Accepts any pathname. | Without pathname, returns a suffix like ?page=2 or #comments. |

Path-based URL contracts

Path-based contracts use @cookbook/pathkit for path pattern matching/building. URLKit adds typed URL state around those paths.

import { url } from '@cookbook/urlkit';

const ArticleUrl = url({
  path: '/articles/{slug:regex([a-z0-9-]+)}',
});

const state = ArticleUrl.parse('/articles/post-1');
// state.pathname: `/articles/${string}`
// state.params.slug: string

ArticleUrl.build({ params: { slug: 'post-1' } });
// '/articles/post-1'

ArticleUrl.match('/articles/post-1');
// true

ArticleUrl.match('/users/post-1');
// false

Path-based build input uses params, not pathname:

ArticleUrl.build({ params: { slug: 'post-1' } });

// Invalid for path-based contracts:
// ArticleUrl.build({ pathname: '/articles/post-1' });

Path params are inferred from the pattern. PathKit owns path constraint syntax, matching, and runtime validation. URLKit delegates those concerns to PathKit and only reads PathKit-compatible constraint chains to infer/coerce parsed params.

Built-in PathKit constraint inference in URLKit:

| Constraint | URLKit parsed param type | Notes | | ------------------- | ------------------------ | -------------------------------------------- | | int | number | Integer values. | | decimal | number | Decimal numeric values. | | min(value) | number | Inclusive numeric minimum. | | max(value) | number | Inclusive numeric maximum. | | range(min,max) | number | Inclusive numeric range. | | uuid | string | Canonical hyphenated UUID values. | | minlength(length) | string | Minimum string length. | | maxlength(length) | string | Maximum string length. | | list(a\| b\| c) | string | Exact pipe-separated list values. | | regex(pattern) | string | Raw regex source without /.../ delimiters. |

Custom PathKit constraints infer string unless a numeric built-in constraint also appears in the chain.

const UserUrl = url({ path: '/users/{id:int}' });

const user = UserUrl.parse('/users/42');
// user.params.id: number

Numeric constraints can be used alone or chained with other constraints:

const ProductUrl = url({ path: '/products/{id:min(1):max(10)?}' });

ProductUrl.parse('/products/2.5').params.id;
// number | undefined

ProductUrl.build({});
// '/products'

The regex constraint receives raw regex source, not a JavaScript regex literal:

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

Inside TypeScript string literals, escape backslashes as needed:

url({ path: '/scores/{id:regex(\\d):min(1)}' });

Path match options

URLKit uses PathKit for path matching. You can pass path match options to parse, safeParse, parseRequest, safeParseRequest, match, and parsePathname. They do not affect normalize or build.

URLKit always uses / as the path delimiter.

| Option | Default | What it does | | ---------------- | ---------- | ------------------------------------------------------- | | trailing | true | Allows one final /. | | sensitive | false | Matches paths without checking letter case. | | strict | false | Lets match return false for path constraint misses. | | end | true | Requires the whole pathname to match. | | wildcardFormat | 'string' | Returns wildcard params as one string. | | decode | false | Keeps path params raw. |

Set path match options on the contract when you want the same behavior everywhere:

const ApiUrl = url(
  { path: '/api' },
  {
    pathMatch: {
      end: false,
    },
  },
);

ApiUrl.parse('/api/users').pathname;
// '/api'

Per-call options override contract options:

ApiUrl.match('/api/users', { end: true });
// false

Use end: false to match only the start of a pathname. When parse uses end: false, state.pathname is the part matched by the contract.

const parsed = ApiUrl.parse('/api/users?page=2', { end: false });

parsed.pathname;
// '/api'

Wildcard params are strings by default:

const FileUrl = url({ path: '/files/{*path}' });

FileUrl.parse('/files/docs/readme').params;
// { path: 'docs/readme' }

Use wildcardFormat: 'array' to get path segments:

FileUrl.parse('/files/docs/readme', { wildcardFormat: 'array' }).params;
// { path: ['docs', 'readme'] }

Path params are raw by default. Use decode: true to decode them:

const UserUrl = url({ path: '/users/{name}' });

UserUrl.parse('/users/John%20Doe').params;
// { name: 'John%20Doe' }

UserUrl.parse('/users/John%20Doe', { decode: true }).params;
// { name: 'John Doe' }

Custom path constraints

URLKit re-exports PathKit's createConstraint and provides global registration helpers for reusable path constraints. Custom constraints infer string params by default unless chained with a numeric constraint. Built-in int, decimal, range(...), min(...), and max(...) infer number. When a PathKit constraint rejects a value, URLKit wraps it as UrlKitError with code: 'invalid-param' and preserves the original PathKit error in error.cause.

import { createConstraint, registerPathConstraint, url } from '@cookbook/urlkit';

const slug = createConstraint({
  parse(paramName, value) {
    if (!/^[a-z0-9-]+$/.test(String(value))) {
      throw new Error(`Path parameter "${paramName}" must be a slug.`);
    }
  },
  verify(paramName, params) {
    if (params.trim()) {
      throw new Error(`Constraint "slug" declared for "${paramName}" does not accept arguments.`);
    }
  },
  toRegExp() {
    return '[a-z0-9-]+';
  },
});

registerPathConstraint('slug', slug);

const ArticleUrl = url({
  path: '/articles/{slug:slug}',
});

ArticleUrl.parse('/articles/hello-world').params.slug;
// string

Use per-contract registration when a constraint should be local to a contract or test:

const ArticleUrl = url({ path: '/articles/{slug:slug}' }, { pathConstraints: { slug } });

Pathless URL contracts

Pathless contracts validate search/hash independently of pathname. pattern is undefined, params is {}, and parse preserves the input pathname.

import { int, url } from '@cookbook/urlkit';

const FiltersUrl = url({
  search: {
    page: int().default(1),
  },
});

FiltersUrl.build({
  search: { page: 2 },
});
// '?page=2'

FiltersUrl.build({
  pathname: '/products',
  search: { page: 2 },
});
// '/products?page=2'

FiltersUrl.parse('/anything?page=3').pathname;
// '/anything'

Search-only helper

import { int, search, string } from '@cookbook/urlkit';

const ProductSearch = search({
  category: string().optional(),
  page: int().default(1),
});

ProductSearch.build({ search: { page: 2 } });
// '?page=2'

Hash-only helper

import { enumOf, hash } from '@cookbook/urlkit';

const DocsHash = hash(enumOf(['intro', 'api']).optional());

DocsHash.parse('/docs#api').hash;
// 'api'

DocsHash.build({ hash: 'api' });
// '#api'

Search params

Runtime search schemas use builders from the main entry.

import { array, boolean, enumOf, int, number, string, url } from '@cookbook/urlkit';

const SearchUrl = url({
  path: '/search',
  search: {
    q: string(),
    page: int().default(1),
    score: number().optional(),
    active: boolean().optional(),
    tags: array(string()).optional(),
    sort: enumOf(['newest', 'popular']).default('newest'),
  },
});

SearchUrl.parse('/search?q=url&page=2&active=true&tags=ts&tags=router');

SearchUrl.build({
  search: {
    q: 'url',
    page: 2,
    active: true,
    tags: ['ts', 'router'],
    sort: 'newest',
  },
});
// '/search?q=url&page=2&active=true&tags=ts&tags=router&sort=newest'

Arrays parse and serialize as repeated params by default. Pass { arrayFormat: 'comma' } to url(...), parse, safeParse, parseRequest, safeParseRequest, match, build, parseSearch, or buildSearch to use comma-separated arrays. Per-call options override the contract-level default, so { arrayFormat: 'repeat' } can force repeated keys on a comma-configured contract.

const TagUrl = url(
  {
    path: '/search',
    search: {
      tags: array(string()).optional(),
    },
  },
  { arrayFormat: 'comma' },
);

TagUrl.parse('/search?tags=ts%2Crouter').search.tags;
// ['ts', 'router']

TagUrl.build({ search: { tags: ['ts', 'router'] } });
// '/search?tags=ts%2Crouter'

TagUrl.build({ search: { tags: ['ts', 'router'] } }, { arrayFormat: 'repeat' });
// '/search?tags=ts&tags=router'

Unknown search params

Unknown search params default to strip.

| Behavior | Result | | ---------- | ----------------------------------------------- | | strip | Remove unknown params from typed state. | | preserve | Put unknown params in state.unknownSearch. | | error | Throw UrlKitError with code invalid-search. |

const QueryUrl = url({
  search: {
    q: string(),
  },
});

QueryUrl.parse('/search?q=router&debug=true');
// search: { q: 'router' }

QueryUrl.parse('/search?q=router&debug=true', { unknownSearch: 'preserve' });
// search: { q: 'router' }
// unknownSearch: { debug: 'true' }

QueryUrl.safeParse('/search?q=router&debug=true', { unknownSearch: 'error' });
// { success: false, error: UrlKitError }

Defaults behavior

parse and normalize always apply defaults. build serializes the values it receives and includes defaults by default.

const Paging = search({
  page: int().default(1),
});

Paging.parse('/products').search;
// { page: 1 }

Paging.build({ search: { page: 1 } });
// '?page=1'

Paging.build({ search: { page: 1 } }, { defaults: 'omit' });
// ''

Default omission compares normalized values, so defaults are compared after the same validation/coercion rules used by the contract.

Dates

import { date, dateTime, search } from '@cookbook/urlkit';

const Reports = search({
  day: date(),
  at: dateTime().optional(),
  createdAt: date({ format: 'unix-seconds' }).optional(),
  updatedAt: date({ format: 'unix-ms' }).optional(),
});

| Builder | Serialized format | | --------------------------------------------- | -------------------------------------- | | date() | Date-only YYYY-MM-DD. | | dateTime() | Strict UTC YYYY-MM-DDTHH:mm:ss.sssZ. | | date({ format: 'dd-MM-yyyy' }) | Strict custom date format string. | | dateTime({ format: 'dd-MM-yyyy HH:mm:ss' }) | Strict custom date-time format string. | | date({ format: { parse, serialize } }) | Custom runtime date codec. | | dateTime({ format: { parse, serialize } }) | Custom runtime date-time codec. | | date({ format: 'unix-seconds' }) | Finite integer seconds. | | date({ format: 'unix-ms' }) | Finite integer milliseconds. |

Custom date and date-time format strings are available in runtime-builder schemas and static router descriptors. Supported tokens are yyyy, MM, dd, HH, mm, ss, and SSS. Static date defaults use serialized values, not Date instances. Static descriptors may use format strings, but not custom { parse, serialize } codecs.

const CustomDate = search({
  from: date({ format: 'dd-MM-yyyy' }),
  at: dateTime({ format: 'dd-MM-yyyy HH:mm:ss' }).optional(),
});

CustomDate.build({
  search: {
    from: new Date('2026-06-02T00:00:00.000Z'),
    at: new Date('2026-06-02T12:30:05.000Z'),
  },
});
// '?from=02-06-2026&at=02-06-2026+12%3A30%3A05'

Object search

object(...) hydrates declared object fields from dotted search keys. Raw search parsing without a schema remains flat.

import { boolean, object, string, search } from '@cookbook/urlkit';

const Filters = search({
  filter: object({
    role: string().optional(),
    active: boolean().optional(),
    'user.name': string().optional(),
  }),
});

Filters.build({
  search: {
    filter: {
      role: 'admin',
      active: true,
      'user.name': 'Ada',
    },
  },
});
// '?filter.role=admin&filter.active=true&filter.user%7E1name=Ada'

Object search key rules:

| Rule | Behavior | | ---------------- | ----------------------------------------------------------------------------- | | Declared objects | Only fields declared with object(...) hydrate nested object values. | | Dot notation | Object fields serialize as field.child=value. | | ~ escaping | ~ becomes ~0. | | . escaping | . becomes ~1. | | URL encoding | Happens after object-key segment escaping. | | Collisions | Ambiguous object/scalar collisions throw UrlKitError with invalid-search. |

Hash

Hashes support optional, required, enum, and defaulted values. Parsed and normalized UrlState always includes a hash property.

import { enumOf, hash, string, url } from '@cookbook/urlkit';

const OptionalHash = hash(enumOf(['intro', 'api']).optional());
OptionalHash.parse('/docs#api').hash;
// 'api'

const RequiredHash = hash(string().required());
RequiredHash.parse('/docs#overview').hash;
// 'overview'

const DefaultHash = url({
  path: '/docs',
  hash: enumOf(['overview', 'comments']).default('overview'),
});

DefaultHash.parse('/docs').hash;
// 'overview'

DefaultHash.build({ hash: 'overview' }, { defaults: 'omit' });
// '/docs'

Safe APIs

Safe APIs return discriminated result objects instead of throwing for ordinary validation errors.

const parsed = UserUrl.safeParse('/users/not-a-number');

if (parsed.success) {
  parsed.data.params.id;
} else {
  parsed.error.code;
}

const normalized = UserUrl.safeNormalize({ params: { id: 'wrong' as never } });
const request = UserUrl.safeParseRequest(new Request('https://example.com/users/42'));

Safe result shape:

type SafeResult<Data> =
  | { readonly success: true; readonly data: Data }
  | { readonly success: false; readonly error: UrlKitError };

Request parsing

parseRequest and safeParseRequest support web-standard Request and request-like { url: string } inputs. Use baseUrl for relative request-like URLs.

UserUrl.parseRequest(new Request('https://example.com/users/42?page=2'));

UserUrl.safeParseRequest({ url: '/users/42?page=2' }, { baseUrl: 'https://example.com' });

No Express, Hono, Fastify, or framework middleware dependency is required. Framework integrations can pass request URLs or use normalize with already-extracted params/search/hash.

Static descriptors

Static descriptors are for tooling and router-compatible definitions. They must remain statically analyzable, so do not use runtime builders in static route definitions.

Good:

const searchDescriptor = {
  page: { type: 'int', default: 1 },
  sort: {
    type: 'enum',
    values: ['newest', 'popular'],
    default: 'newest',
  },
} as const;

Bad:

import { int } from '@cookbook/urlkit';

const searchDescriptor = {
  page: int().default(1),
};

Compile static descriptors through @cookbook/urlkit/static:

import { compileStaticUrl } from '@cookbook/urlkit/static';

const ProductUrl = compileStaticUrl({
  path: '/products/{id:int}',
  search: searchDescriptor,
  hash: { type: 'enum', values: ['details', 'reviews'], optional: true },
});

ProductUrl.parse('/products/42?sort=popular#details');

Router-runtime usage

@cookbook/urlkit/router-runtime contains framework-agnostic helpers for router packages. It does not define routes, route IDs, route trees, loaders, middleware, components, or hooks.

import {
  buildSearch,
  createRouteUrlContract,
  parseHash,
  parseSearch,
  patchSearch,
} from '@cookbook/urlkit/router-runtime';

const routeDescriptor = {
  path: '/articles/{slug:regex([a-z0-9-]+)}',
  search: {
    ref: { type: 'string', optional: true },
    page: { type: 'int', default: 1 },
    publishedOn: {
      type: 'date',
      format: 'dd-MM-yyyy',
      optional: true,
    },
    scheduledAt: {
      type: 'date-time',
      format: 'dd-MM-yyyy HH:mm:ss',
      optional: true,
    },
  },
  hash: { type: 'enum', values: ['comments', 'share'], optional: true },
} as const;

const ArticleUrl = createRouteUrlContract(routeDescriptor);

ArticleUrl.parse('/articles/post-1?ref=email#comments');
// Router-runtime params default to raw strings.

const parsed = parseSearch('?page=2&publishedOn=02-06-2026', {
  schema: routeDescriptor.search,
});
const partial = ArticleUrl.parseSearch(
  '/articles/post-1?page=2&publishedOn=02-06-2026&scheduledAt=foo',
  {
    invalidSearch: 'omit',
  },
);
const next = buildSearch(
  { page: 3, scheduledAt: new Date('2026-06-02T12:30:05.000Z') },
  { schema: routeDescriptor.search },
);
const patched = patchSearch('?page=2&ref=email', { page: 3 }, { schema: routeDescriptor.search });
const section = parseHash('#comments', routeDescriptor.hash);
const missingSection = parseHash('#overview', routeDescriptor.hash, { invalidHash: 'omit' });

Additional router-runtime helpers:

import {
  buildHash,
  normalizeHash,
  omitSearch,
  pickSearch,
  replaceSearch,
} from '@cookbook/urlkit/router-runtime';

Use { params: 'parsed' } with createRouteUrlContract when a router wants URLKit to parse numeric PathKit constraints such as int, decimal, and range to numbers.

Error handling

All URLKit validation and descriptor errors use UrlKitError.

import { UrlKitError } from '@cookbook/urlkit';

try {
  UserUrl.parse('/users/not-a-number');
} catch (error) {
  if (error instanceof UrlKitError) {
    console.log(error.code, error.path);
  }
}

| Code | Meaning | | -------------------- | ------------------------------------------------------------------------------------------ | | invalid-url | URL input could not be parsed as a URL. | | path-mismatch | URL pathname does not satisfy the path contract. | | missing-param | Required path param is missing. | | invalid-param | Path param is invalid. Constraint failures preserve the original PathKit error in cause. | | missing-search | Required search field is missing. | | invalid-search | Search value, unknown search behavior, or object search shape is invalid. | | invalid-hash | Hash value is missing or invalid. | | invalid-descriptor | Contract/schema/static descriptor is invalid at construction/compile time. |

TypeScript inference

URLKit infers path params, pathnames, search values, and hash values from the contract.

const UserUrl = url({
  path: '/users/{id:int}',
  search: {
    tab: enumOf(['profile', 'settings']).default('profile'),
  },
  hash: enumOf(['activity', 'comments']).optional(),
});

const state = UserUrl.parse('/users/42?tab=settings#activity');

state.pathname;
// `/users/${number}`

state.params.id;
// number

state.search.tab;
// 'profile' | 'settings'

state.hash;
// 'activity' | 'comments' | undefined

Pathless contracts use pathname: string because they validate search/hash independently of the path.

const Query = search({ q: string() });
const state = Query.parse('/anything?q=url');

state.pathname;
// string

Framework boundary

URLKit core is intentionally framework-agnostic:

  • no React APIs
  • no framework middleware
  • no route definitions or route trees
  • no loaders/actions
  • no Express/Hono/Fastify/Next.js adapters

Router and framework packages can consume URLKit contracts through serialized URLs, Request, request-like { url: string }, or structured normalize input.

Testing and development

pnpm install
npm run typecheck
npm test
npm run build

No lint script is currently configured in package.json.