URLPattern API: Route Matching Without a Regex Library
AI generated
JS
() =>
JavaScript · Browser APIs · Routing
URLPattern API
Route matching without a regex library

A hand-rolled router usually ends up as a thicket of regex patterns meant to recognize exact URL paths and extract parameters, barely readable and error-prone around special characters. The URLPattern API solves exactly this problem declaratively and natively in the browser.

15 min read URLPattern API Routing Service Worker Pattern Matching

1. The problem with handwritten regex routers

Anyone who has written their own router for a single-page app or a service worker knows the pattern: a path like /products/:id/reviews/:reviewId gets translated into a regex like /^\/products\/([^/]+)\/reviews\/([^/]+)$/, complete with manual group assignment via match[1], match[2] and so on. It works, but it's hard to read, error-prone around URL-encoded special characters, and makes code review unnecessarily tedious.

The URLPattern API solves this by providing a declarative matching syntax, modeled after familiar router syntax like Express or Vue Router, directly as a browser standard. /products/:id/reviews/:reviewId becomes a self-describing pattern object that automatically extracts named groups, no hand-written regex required.

2. Basics: creating and testing a URLPattern object

A URLPattern is instantiated with an object of URL components, usually just pathname suffices, but it can also include protocol, hostname, port, search and hash to match complete URLs instead of just paths. The simplest check is pattern.test(url), which only returns true or false for whether a given URL matches the pattern.

For the actual extraction of parameters, pattern.exec(url) returns a detailed result object or null if there's no match. This object contains a groups field per URL component with the extracted named-group values, so no manual indexing like with classic regex matches is needed.


// Create a simple pattern and test it
const pattern = new URLPattern({ pathname: '/products/:id' });

console.log(pattern.test('https://mironsoft.de/products/42')); // true
console.log(pattern.test('https://mironsoft.de/categories/42')); // false

const result = pattern.exec('https://mironsoft.de/products/42');
console.log(result.pathname.groups.id); // "42"

3. Named groups: extracting parameters declaratively

Named groups are the central tool for parameterized routes. A colon followed by an identifier, such as :id or :slug, defines a named group that captures an arbitrary path segment value and shows up under exactly that name in the groups object of the exec() result. Multiple named groups within a pattern are extracted independently of one another.

Unlike hand-written regex groups, order is irrelevant here; only the name matters. That makes refactoring considerably safer: if a named group is moved within the pattern or another one is inserted before it, code accessing groups.id remains correct unchanged, whereas with numerically indexed regex groups every shift would have shifted every subsequent index.


// Multiple named groups in one pattern
const pattern = new URLPattern({
  pathname: '/products/:category/:id/reviews/:reviewId',
});

const result = pattern.exec('https://mironsoft.de/products/laptops/42/reviews/7');

console.log(result.pathname.groups);
// { category: 'laptops', id: '42', reviewId: '7' }

4. Wildcards and optional segments

Besides named groups, the URLPattern API supports wildcards via the asterisk (*), which captures any number of characters including slashes, handy for catch-all routes like static file servers or 404 fallbacks. A pattern like /assets/* matches any path starting with /assets/, regardless of how many further segments follow.

Optional segments are marked with a question mark after the group, for example /blog/:slug? covers both /blog and /blog/my-article. This syntax deliberately mirrors already established router libraries like path-to-regexp, so developers experienced with Express or Vue Router immediately feel at home without having to learn the spec from scratch.


// Wildcard for catch-all routes
const assetsPattern = new URLPattern({ pathname: '/assets/*' });
console.log(assetsPattern.test('https://mironsoft.de/assets/img/logo.svg')); // true

// Optional segment
const blogPattern = new URLPattern({ pathname: '/blog/:slug?' });
console.log(blogPattern.test('https://mironsoft.de/blog')); // true
console.log(blogPattern.test('https://mironsoft.de/blog/my-article')); // true

5. Use inside a service worker router

A classic place to use the URLPattern API is a service worker's fetch handler, which needs to apply different caching strategies depending on the requested URL, for example cache-first for images, network-first for API calls, and stale-while-revalidate for HTML pages. Instead of manually checking with startsWith() or regex, routes can be defined as a list of URLPattern objects paired with a strategy.

This structure is considerably more maintainable than nested if/else blocks, because new routes are simply added as another entry in the list without touching existing conditions. The fetch handler iterates over all routes once and applies the first matching strategy, a pattern that can be reused almost identically in a client-side router.


// sw.js: route table with URLPattern
const routes = [
  { pattern: new URLPattern({ pathname: '/assets/*' }), strategy: cacheFirst },
  { pattern: new URLPattern({ pathname: '/api/*' }), strategy: networkFirst },
  { pattern: new URLPattern({ pathname: '/*' }), strategy: staleWhileRevalidate },
];

self.addEventListener('fetch', (event) => {
  const route = routes.find((r) => r.pattern.test(event.request.url));
  if (route) {
    event.respondWith(route.strategy(event.request));
  }
});

6. Use inside a client router

On the client side, the URLPattern API pairs excellently with the Navigation API, letting you decide inside a navigate event handler which route is responsible and which parameters to extract from the URL. Instead of custom parsing logic, exec() handles both matching and parameter extraction in a single call.

A simple router iterates over a configuration list of pattern plus associated component, finds the first match, and passes the extracted named groups directly as props to the responsible view function. This often reduces router implementations to just a few lines of core logic, with the rest being pure configuration.


// Simple client router using URLPattern
const routeTable = [
  { pattern: new URLPattern({ pathname: '/products/:id' }), view: renderProductView },
  { pattern: new URLPattern({ pathname: '/categories/:slug' }), view: renderCategoryView },
  { pattern: new URLPattern({ pathname: '/*' }), view: renderNotFoundView },
];

function resolveRoute(url) {
  for (const route of routeTable) {
    const match = route.pattern.exec(url);
    if (match) {
      return route.view(match.pathname.groups);
    }
  }
}

7. Direct comparison to handwritten regex patterns

The most obvious advantage of the URLPattern API is readability: /products/:id/reviews/:reviewId is understandable at a glance, while the equivalent regex with slash escaping and non-capturing groups demands close attention even from experienced developers. Mistakes such as a forgotten escape of a dot in the path lead to silent regex bugs, pitfalls the URLPattern syntax avoids from the start.

The second advantage is built-in URL component awareness: a URLPattern can simultaneously match protocol, hostname, port and pathname, for example to only capture requests from a specific subdomain over HTTPS, while an equivalent regex would have to parse the complete URL as a single string and account for URL-encoding quirks itself.

A third, often underestimated point is testability: since a URLPattern object consists of a clear configuration, it's easy to check against different example URLs in unit tests without test authors needing to understand regex syntax themselves. A reviewer can tell from the pattern definition alone which URLs are meant to match, something that with a raw regex often only gets clarified by trial and error in the console.

8. Browser support and polyfill

The URLPattern API has been natively available in Chrome and Edge since version 95, but is not yet implemented in Firefox or Safari at the time this article was written. For production use in publicly accessible applications, a polyfill such as urlpattern-polyfill is therefore worthwhile, providing the same API surface based on its own parser.

Since the polyfill implements the same constructor signature and the same methods, switching between the native API and the polyfill can be handled with a simple feature-detection pattern at the start of the application, without needing to adjust the rest of the router code.

In service workers, the polyfill's bundle size deserves particular attention, since every extra import lengthens the worker's activation time. It's therefore advisable to load the polyfill only when feature detection actually finds the native API missing, rather than shipping it unconditionally for every user in the main bundle.


// Only load the polyfill if the native API is missing
async function ensureURLPattern() {
  if (typeof URLPattern === 'undefined') {
    await import('urlpattern-polyfill');
  }
}

await ensureURLPattern();
// URLPattern is guaranteed to be available from here on

9. Conclusion: declarative matching as the standard router tool

The URLPattern API replaces handwritten regex routers with a readable, standardized syntax that automatically extracts named groups while taking the complete URL components into account instead of just individual strings. In both service worker routers for caching strategies and client routers for SPA navigation, it significantly reduces boilerplate.

As long as browser support isn't complete, a polyfill for Firefox and Safari remains advisable, but the API surface itself is already the right standard for new router implementations. The table below compares core aspects between URLPattern and handwritten regex.

Aspect Handwritten regex URLPattern API Advantage
Readability Escaping, non-capturing groups Self-describing :name syntax Understandable at a glance without regex knowledge
Parameter extraction Manual indexing via match[1] groups object with names Robust against refactoring
URL components Complete string must be parsed protocol/hostname/pathname separate Precise matching of individual parts
Browser support Everywhere (pure JS) Native in Chromium, else polyfill Regex usable as a fallback option

Mironsoft

Modern browser APIs, performance, and maintainable JavaScript

JavaScript that holds up in the real browser, not just in the tutorial?

We review existing frontend code for outdated patterns, unnecessary dependencies, and performance traps, then replace them with modern, native browser APIs that mean less bundle weight and less maintenance burden.

Code Review

Systematically finding outdated patterns, unnecessary dependencies, and memory leaks.

Performance Optimization

Improving bundle size, load time, and runtime performance with modern APIs.

Modernization

Deliberately introducing native browser APIs instead of heavy libraries.

10. Summary

URLPattern API: The Key Facts at a Glance

Named groups

:name syntax automatically extracts parameters into the groups object

Wildcards

Asterisk (*) and question mark (?) for catch-all and optional segments

Use cases

Service worker caching routers and client-side SPA routers

Support

Native in Chromium, polyfill available for Firefox and Safari

11. FAQ: URLPattern API: The Key Facts at a Glance

1What problem does the URLPattern API solve compared to regex?
It replaces handwritten regex patterns for URL matching with a readable, declarative syntax that automatically extracts parameters via named groups.
2How do I create named groups?
With a colon followed by an identifier in the path, such as /products/:id. The extracted value appears in the groups object of the exec() result under the name id.
3What's the difference between test() and exec()?
test() only returns true or false for whether a URL matches the pattern. exec() additionally returns the full match object with all extracted named groups, or null if there's no match.
4How do I define a catch-all route?
With an asterisk as a wildcard, for example /assets/*, which captures any number of characters including slashes.
5Can URLPattern also match hostname or protocol?
Yes, besides pathname the pattern object can also include protocol, hostname, port, search and hash to check complete URLs instead of just paths.
6How do I make a segment optional?
With a question mark after the named group, for example /blog/:slug?, which covers both /blog and /blog/my-article.
7Where is the URLPattern API typically used?
In service worker routers for different caching strategies depending on the URL, and in client-side routers for SPA navigation, often combined with the Navigation API.
8Which browsers support the API natively?
Chrome and Edge since version 95. Firefox and Safari don't support it natively yet at the time this article was written.
9Is there a polyfill for unsupported browsers?
Yes, urlpattern-polyfill implements the same API surface and can be lazily loaded via feature detection only when needed.
10Is the syntax modeled after existing router libraries?
Yes, the :name syntax deliberately mirrors established libraries like path-to-regexp, which is used among others by Express and Vue Router.