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.
Table of Contents
- 1. The problem with handwritten regex routers
- 2. Basics: creating and testing a URLPattern object
- 3. Named groups: extracting parameters declaratively
- 4. Wildcards and optional segments
- 5. Use inside a service worker router
- 6. Use inside a client router
- 7. Direct comparison to handwritten regex patterns
- 8. Browser support and polyfill
- 9. Conclusion: declarative matching as the standard router tool
- 10. Summary
- 11. FAQ
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