Stop Writing Regex to Match URLs — The Browser Already Can
A service worker's route regex was missing one anchor and started caching live edit forms as if they were read-only pages. URLPattern, native since 2021, doesn't let you make that mistake.

Priya was three paragraphs into rewriting a support ticket when the page flashed and her draft reverted to what it had looked like an hour earlier. She hadn't refreshed. Nobody had.
The service worker had.
It was running a cache-first strategy for ticket pages — fetch once, serve from cache after that, so the dashboard felt instant on a flaky connection. The intent was to cache /tickets/482, the read-only view, and leave /tickets/482/edit alone, since an edit form is exactly the page you never want served stale. Here's the line that decided which was which:
Spot it yet? Read it once more before you scroll.
/^\/tickets\/\d+/ anchors the start of the string — ^ — but never anchors the end. So it matches /tickets/482. It also matches /tickets/482/edit, /tickets/482/history, and /tickets/482-anything-at-all, because "one or more digits after /tickets/" is true of all of them. The regex was never wrong about what it checked. It just never checked enough.
The one-character fix is obvious once you see it:
Ship that and you'll hit the next edge case within a week: a trailing slash (/tickets/482/) now fails to match, because $ demands nothing comes after the digits — not even a slash. Add \/? before the $ and you've fixed that one. Then someone deep-links to /tickets/482?tab=history and the query string breaks the anchor again, because pathname on some code paths actually holds the full URL. Each fix is a patch on the last, and every patch is a chance to reintroduce the first bug in a new shape.
This is the part nobody tells you about hand-rolled URL matching: it isn't hard because regex is hard. It's hard because "does this path match this shape" has a dozen boundary conditions, and a hand-written pattern only encodes the ones you happened to think of on the day you wrote it.
The browser has had a purpose-built answer since 2021, and it isn't a regex — it's URLPattern, a global constructor that matches and parses URLs the way a router does, natively:
No ^, no $, no \d+. /tickets/:id matches the whole pathname component by default — that's not a lucky accident of this example, it's the core design decision. A URLPattern pattern is exact-match unless you explicitly tell it otherwise. There's no anchor to forget, because there's nothing unanchored to begin with.
That was invented for this exact scenario, by the way — it originally shipped to give service workers a real routing syntax for fetch event handlers instead of everyone writing their own regex. You're not reaching for an obscure API here; you're reaching for the one built by people who'd already hit Priya's bug.
:id is a named group — it captures a path segment and gives it a name you can read back later. That's the syntax doing double duty: matching and extracting, in one string.
Three more pieces cover almost everything else you'll need:
Four building blocks — a literal, :name, *, and {…}? — cover the shapes that used to take a paragraph of regex to express, and read back close to plain English.
test() only answers yes or no. When you need the actual id, call exec() instead:
exec() returns null on no match, and otherwise an object with one entry per URL component you matched against (pathname, search, hash, and so on), each carrying a groups object keyed by name. No manual .split("/"), no match[1] where you have to remember what group 1 was.
Runs right in your browser — poke at it and watch the concept react live.
Back in the service worker, the honest fix isn't a smarter regex — it's naming the two things that were always different:
ticketView never matches /tickets/482/edit — not because of a character you remembered to add, but because the pattern simply describes a shorter path than the one it's being compared against. There's no anchor to audit in a future code review, because exactness was never optional.
URLPattern matches and extracts. It doesn't dispatch, and it doesn't rank overlapping routes by specificity the way a full router does — if you hand it a list of ten patterns, you're still the one deciding which to check first. For a page with dozens of nested, code-split routes, you'll still likely reach for a router library that happens to use something like this under the hood. But for the far more common case — a service worker's allowlist, an analytics filter deciding which URLs to sample, a tiny client-side router that only ever needed four routes — you don't need the library. You need the four lines above.
It's also worth checking your target audience before you lean on it fully: it's been in Chrome and Edge since 2021, and Firefox and Safari caught up through 2025, so by now it's safe to reach for directly rather than through a polyfill in almost any modern app.
The regex wasn't wrong about what it checked for — it just never had to promise it checked everything, and one missing $ let an edit form get served like a snapshot. URLPattern doesn't make that promise implicit. Exactness is the default, not a character you have to remember.
Next time you catch yourself writing /^\/something\/\d+/ and mentally listing the edge cases you'll patch in later — trailing slash, query string, that one route with two segments — stop and reach for new URLPattern({ pathname: "..." }) instead. What's the last regex you wrote to match a URL? I'd bet it's missing an anchor somewhere too.
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.