10LOC
Chrome APIsintermediate

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 URLPattern is 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 }) returns null on no match, or a result object with one sub-object per URL component (.pathname, .search, etc.), each carrying a .groups record of the named segments that pattern captured — match.pathname.groups.id for the example above.
  • matchRoute walks 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 include URLPattern yet (see Gotchas) — everything after that line is fully typed, no any leaks into matchRoute or the route table.

Gotchas

  • TypeScript hasn't added URLPattern to 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/web project 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.
  • :id matches 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 or URL object, 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+) constrains id to 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 syntax URLPattern borrows from and replaces for straightforward cases; still relevant for advanced transform/compile features URLPattern doesn't cover.
  • Service Worker static routing (InstallEvent.addRoutes()) — uses the same URLPattern matching to declare cacheable routes without running JS on every request.