
The Native Router
The URLPattern API is finally standardizing how we match and parse URLs, making bulky regex-based routing libraries a thing of the past.
For years, if you wanted to determine if a URL matched /user/:id and extract that ID, you had two choices: pull in a heavy third-party library like path-to-regexp or write a custom regular expression that looked like a cat walked across your keyboard. Neither felt particularly "web native."
Enter the URLPattern API. It’s the platform’s answer to the routing problem, providing a built-in way to match URLs against a pattern and extract semantic data without the baggage of a bulky router library.
The "Hello World" of Matching
At its simplest, URLPattern takes a pattern object (or a string) and tells you if a URL fits the bill. It handles the annoying parts—like trailing slashes and port numbers—so you don't have to.
// Define the shape of the URL you're looking for
const pattern = new URLPattern({ pathname: '/products/:category/:id' });
// Check a URL
const url = 'https://shop.example.com/products/shoes/123';
console.log(pattern.test(url)); // trueI love this because it's readable. If I glance at :category, I know exactly what’s happening. No more squinting at \/products\/([^/]+)\/([^/]+) and praying I got the capture groups right.
Extracting Data (The Good Stuff)
Matching is fine, but we usually need the actual values inside those placeholders. The exec() method is where the magic happens. It returns an object containing all the captured groups, neatly organized.
const blogPattern = new URLPattern({ pathname: '/posts/:slug' });
const match = blogPattern.exec('https://myblog.com/posts/why-i-love-web-apis');
if (match) {
const { slug } = match.pathname.groups;
console.log(`Fetching data for: ${slug}`);
// Output: "Fetching data for: why-i-love-web-apis"
}This returns a structured object that includes groups for the protocol, hostname, port, and even the search (query) parameters. It’s a full-service parsing engine that lives right in the browser.
Why Should You Care?
You might be thinking, "My current router works fine, why switch?"
First, bundle size. If you’re building a small utility or a lightweight component, importing a routing library just to parse three URLs is overkill. URLPattern is zero-bytes added to your payload.
Second, consistency. Every routing library has its own flavor of syntax. Some use :id, some use {id}, some use regex segments. URLPattern is the standard. It’s what the platform uses, meaning your knowledge is now transferable across any framework or vanilla project.
Query Params and Wildcards
The API isn't limited to just paths. You can match against the domain or even specific query parameters. This is incredibly useful for feature flagging or handling specific UTM codes.
const promoPattern = new URLPattern({
pathname: '/checkout',
search: 'coupon=*'
});
const result = promoPattern.exec('https://site.com/checkout?coupon=SUMMER24');
if (result) {
console.log('User has a coupon!');
console.log(result.search.groups[0]); // "SUMMER24"
}The * is a wildcard. It’s greedy. It’ll grab whatever is there. I’ve found this much more reliable than trying to manually parse URLSearchParams every time I need a single value.
The "Gotchas" (Because there are always gotchas)
While I'm a huge fan, there are two things that tripped me up when I first started using it:
1. Browser Support: As of right now, it’s a Chromium-first party. It’s available in Chrome, Edge, and Deno/Node. Firefox and Safari are still "considering" it, though there is a very solid polyfill available if you want to use it everywhere today.
2. The Base URL: If you pass a relative string to test() or exec(), it might fail if the pattern expects a full URL. To be safe, I usually pass the full window.location.href or ensure my patterns are explicitly defined for the parts I care about.
A Practical Mini-Router
To show how this looks in a real-ish scenario, here is a tiny, 10-line router function that handles navigation:
const routes = [
{ pattern: new URLPattern({ pathname: '/' }), render: () => 'Home' },
{ pattern: new URLPattern({ pathname: '/user/:id' }), render: (groups) => `User: ${groups.id}` },
];
function handleRoute(url) {
for (const route of routes) {
const match = route.pattern.exec(url);
if (match) {
return route.render(match.pathname.groups);
}
}
return '404 Not Found';
}
console.log(handleRoute('https://app.com/user/dan')); // "User: dan"No dependencies. No complex configuration. Just native Web APIs doing what they were designed to do. We're finally reaching a point where the "Standard Library" of the web is powerful enough to handle the chores that used to require a dozen NPM packages. It's about time.


