URLPattern for route matching without a regex library
Published April 16, 2027
// TypeScript hasn't bundled URLPattern into its DOM types yet, so this repo's tsconfig
// doesn't know about it either. One cast at the constructor keeps everything below fully
// typed instead of leaking `any` through the rest of the file.
type UrlMatch = { pathname: { groups: Record<string, string | undefined> } };
type UrlPatternLike = { exec: (input: { pathname: string }) => UrlMatch | null };
const URLPattern = (globalThis as any).URLPattern as new (init: { pathname: string }) => UrlPatternLike;
type Route = { pattern: UrlPatternLike; handler: (params: Record<string, string | undefined>) => string };
const routes: Route[] = [
{ pattern: new URLPattern({ pathname: "/users/:id" }), handler: (p) => `user ${p.id}` },
{ pattern: new URLPattern({ pathname: "/posts/:slug/comments" }), handler: (p) => `comments on ${p.slug}` },
{ pattern: new URLPattern({ pathname: "/posts/:slug" }), handler: (p) => `post ${p.slug}` },
];
export const matchRoute = (pathname: string): string | null => {
for (const { pattern, handler } of routes) {
const match = pattern.exec({ pathname });
if (match) return handler(match.pathname.groups);
}
return null;
};What
URLPattern is a built-in URL-matching primitive: construct one with a glob-like pattern (/users/:id, wildcards, optional segments) against any URL component — pathname, search, hostname, protocol — and match or extract named groups from a real URL, with no dependency.
Why it matters
Every router — Express, Next.js, React Router, Hono, most hand-rolled ones — eventually reimplements "match /users/:id against a real URL and pull out id," usually via a small library (path-to-regexp and its many forks) that compiles the pattern to a regex under the hood. URLPattern puts that exact primitive at the platform level instead of the userland level: same :param/*/{} syntax, but standardized, shipped in the browser, and matchable against any individual URL component rather than only the full string — so a single pattern object can constrain protocol, hostname, and path at once if you need that.
How it works
The snippet builds a tiny route table and matches a pathname against it in order:
- Each
URLPatternis constructed with just{ pathname: "..." }— components left unspecified (protocol, hostname, search) default to matching anything, which is exactly what you want for path-only routing. pattern.exec({ pathname })returnsnullon no match, or a result object with one sub-object per URL component (.pathname,.search, etc.), each carrying a.groupsrecord of the named segments that pattern captured —match.pathname.groups.idfor the example above.matchRoutewalks the routes in order and returns the first handler whose pattern matched — same "first match wins" semantics as any router, so ordering (specific routes before catch-alls) still matters.- The one cast at the top (
(globalThis as any).URLPattern as new (...) => UrlPatternLike) exists because TypeScript's bundled DOM types don't includeURLPatternyet (see Gotchas) — everything after that line is fully typed, noanyleaks intomatchRouteor the route table.
Gotchas
- TypeScript hasn't added
URLPatternto its bundled DOM types as of this writing, despite the API itself shipping in every major browser — if you see "Cannot find name 'URLPattern'" in your own editor, that's a lib gap, not a sign the API is unsupported. The@types/webproject tracks newer web APIs faster than TypeScript's own DOM lib if you want the real types instead of the local cast this snippet uses. :idmatches a single path segment (stops at the next/) by default — it is not a catch-all. For matching multiple segments, use*, and for optional segments, wrap them in{ }(e.g.{/:id}?).exec()also accepts a full URL string orURLobject, not just a structured{ pathname }object — passing a structured object with only some components set is convenient for path-only routing, but double-check what you intended to leave unconstrained if a pattern was built with other components (protocol,hostname) specified.- Pattern syntax also accepts an inline regex per segment (
:id(\\d+)constrainsidto digits) — useful for the cases a plain named group is too loose for, without reaching for a separate validation step.
Browser + runtime support: Baseline since September 2025 — Chrome/Edge (supported since 2021, now finalized), Firefox, and Safari all ship it. Off the browser, Node 23.8+ exposes it via import { URLPattern } from "node:url" (global as of Node 24), Deno has shipped it as part of its web-standards alignment, and Bun added it in its 1.3.x line — route-matching code written against URLPattern now runs unmodified in the browser and across the major JS runtimes, no library required anywhere in that list.
Related
path-to-regexp— the library whose syntaxURLPatternborrows from and replaces for straightforward cases; still relevant for advanced transform/compile featuresURLPatterndoesn't cover.- Service Worker static routing (
InstallEvent.addRoutes()) — uses the sameURLPatternmatching to declare cacheable routes without running JS on every request.