route-pattern
Type-safe URL matching and href generation for JavaScript. route-pattern supports path variables, wildcards, optionals, search constraints, and full-URL patterns with predictable ranking.
Features
- Type-safe - Infer params from patterns for compile-time correctness
- Expressive - Variables, wildcards, optionals, and search constraints
- Full URL support - Match protocol, hostname, port, pathname, and search
- Simple & deterministic ranking - Predictable left-to-right priority for static, variable, and wildcard patterns
- Fast - Trie-based matching for scalable performance
- Modular - Import only the features you need to for smaller bundles
- Runtime agnostic - Works across Node.js, Bun, Deno, Cloudflare Workers, and browsers
Installation
npm i remix
Quick example
import { createMultiMatcher } from 'remix/route-pattern/match'
let matcher = createMultiMatcher<{ name: string }>()
matcher.add('blog/:slug', { name: 'blog-post' })
matcher.add('api(/v:version)/*path', { name: 'api' })
matcher.add('http(s)://:region.cdn.com/assets/*file.:ext', { name: 'assets' })
let match = matcher.match('https://example.com/blog/v3')
match?.pattern.toString()
match?.params
match?.data
import { createHref } from 'remix/route-pattern/href'
createHref('blog/:slug', { slug: 'v3' })
createHref('api(/v:version)/*path', { version: '2', path: 'users/profile' })
createHref('http(s)://:region.cdn.com/assets/*file.:ext', {
region: 'us-west',
file: 'images/logo',
ext: 'png',
})
API at a glance
remix/route-pattern | Parse and stringify patterns. |
remix/route-pattern/href | Generate hrefs for patterns with type safe params. |
remix/route-pattern/match | Match against one pattern with type inference for params, or match against many patterns with deterministic ranking and attached data. |
remix/route-pattern/join | Combine two patterns into one. Override protocol, hostname, port. Join pathnames. Merge search constraints. |
remix/route-pattern/specificity | Rank matches by specificity. |
For in-depth reference, visit the route-pattern API docs
Examples in this README use remix/route-pattern/* imports. The same APIs are also available from the direct package entrypoints: @remix-run/route-pattern, @remix-run/route-pattern/href, @remix-run/route-pattern/match, @remix-run/route-pattern/join, and @remix-run/route-pattern/specificity.
Pattern syntax
Protocol
Protocol must be http, https, or http(s):
'https://example.com'
'http(s)://example.com'
Hostname & pathname
Variables capture dynamic segments using :name:
'users/:id'
'blog/:year-:month-:day/:slug'
Wildcards match multi-segment paths using *name:
'files/*path'
'node_modules/*package/dist/index.js'
'files/*'
Optionals make parts optional using ():
'api(/v:version)/users'
'blog/:slug(.html)'
'docs(/guides/:category)'
'api(/v:major(.:minor))'
While variables, wilcards, and optionals are most prevalent in pathnames, you can also use them in hostnames:
':tenant.example.com/dashboard'
'(www.)example.com/blog/:slug(.html)'
'*.example.com/files/*path'
'(:locale.)example.com/docs(/:section)'
Escape characters with \:
'time/12\\:30'
'calculator/2\\*3'
'wiki/Mercury_\\(planet\\)'
'wiki/AC\\/DC'
Search
Search constraints narrow matches using ?key or ?key=value:
'search?q'
'search?q=routing'
Match URLs
Match against a single pattern
Use createMatcher when you have one pattern and want params inferred from that exact pattern.
import { createMatcher } from 'remix/route-pattern/match'
const url: string | URL =
let blogMatcher = createMatcher('blog/:slug')
blogMatcher.match(url)?.params
let docsMatcher = createMatcher('://(:tenant.)host.com/docs/*path.:ext')
docsMatcher.match(url)?.params
Matchers accept absolute URL strings or URL objects. Relative strings such as /blog/v3 are not accepted because matching uses the platform URL parser; wrap relative paths with a known origin before matching them.
Match against multiple patterns
Use createMultiMatcher when you need to match many patterns and attach your own data to each match.
import { createMultiMatcher } from 'remix/route-pattern/match'
let matcher = createMultiMatcher<string>()
matcher.add('/', 'home')
matcher.add('blog/:slug', 'blog-post')
matcher.add('api(/v:version)/*path', 'api')
matcher.match('https://example.com/blog/v3')
matcher.match('https://example.com/api/v2/users/profile')
The matched pattern is only known at runtime, so matched params are not inferred when matching with createMultiMatcher.
Each match returns:
url: the URL object that was matched
pattern: the matched RoutePattern
data: the data attached with matcher.add(pattern, data)
params: captured param values
paramsMeta: hostname and pathname param metadata
paramsMeta.hostname and paramsMeta.pathname are arrays of { type, name, value, begin, end } entries. The offsets are measured after URL normalization. A pattern with no hostname matches any hostname, represented in paramsMeta.hostname as an unnamed wildcard entry.
Set ignoreCase: true to make pathname matching case-insensitive. Hostname matching is always case-insensitive, and search constraints are always case-sensitive.
let matcher = createMatcher('/Docs/:slug', { ignoreCase: true })
matcher.match('https://example.com/docs/Intro')?.params
Ranking matches by specificity
When multiple patterns match the same URL, route-pattern chooses the most specific match deterministically. Matches are ranked left-to-right, character-by-character:
- Explicit protocol and port constraints are more specific than omitted constraints.
- Static hostnames are more specific than dynamic hostnames, which are more specific than omitted hostnames.
- Static characters are more specific than variables.
- Variables are more specific than wildcards.
- Earliest difference decides the winner.
This is the same ranking used by createMultiMatcher.
For advanced use cases, /specificity provides comparison utilities: lessThan, greaterThan, equal, descending, ascending, compare. lessThan(a, b) returns true when match a is less specific than match b. For example:
import { createMultiMatcher } from 'remix/route-pattern/match'
import { descending } from 'remix/route-pattern/specificity'
let matcher = createMultiMatcher()
matcher.add('files/*path', null)
matcher.add('files/:name', null)
matcher.add('files/readme.md', null)
let matches = matcher.matchAll('https://example.com/files/readme.md')
matches.sort(descending).map((match) => match.pattern.toString())
Generate hrefs
createHref turns a pattern and params into a URL string. Required variables and wildcards must be provided, while params inside optional groups may be omitted.
import { createHref } from 'remix/route-pattern/href'
createHref('blog/:slug', { slug: 'v3' })
createHref('api(/v:version)/*path', { path: 'users/profile' })
createHref('api(/v:version)/*path', { version: '2', path: 'users/profile' })
createHref('http(s)://:region.cdn.com/assets/*file.:ext', {
region: 'us-west',
file: 'images/logo',
ext: 'png',
})
createHref('blog/:slug?ref=docs', { slug: 'v3' }, { utm_source: 'newsletter' })
createHref() throws CreateHrefError when it cannot safely generate an href. The error exposes stable structured details on error.details; the string message is for humans.
Common failures include missing required params, nameless wildcards, invalid hostname params, empty pathname variables, and origin patterns that specify a protocol or port without a concrete hostname.
Note: optional groups without params are included in the generated href:
createHref('todos(/new)')
createHref('products(.json)')
Parse & stringify patterns
You can explicitly parse and stringify patterns. Create a RoutePattern with RoutePattern.parse and use the methods and helpers below instead of reading parsed token internals.
import { getRoutePatternCaptures, RoutePattern } from 'remix/route-pattern'
let pattern = RoutePattern.parse('://:tenant.example.com/blog/:slug(/*path)')
pattern.toString()
pattern.toJSON()
getRoutePatternCaptures(pattern)
All APIs that take a pattern arg accept string or a parsed RoutePattern.
TIP: For high-performance scenarios, you can parse patterns ahead of time to avoid reparsing them on every call.
RoutePattern.toJSON() returns a RoutePatternJSON object with serialized protocol, hostname, port, pathname, and search fields. RoutePattern.parse() throws ParseError for malformed sources; the error exposes stable type, source, and index fields.
The public support types are:
RoutePatternCapture from remix/route-pattern
RoutePatternJSON from remix/route-pattern
CreateHrefErrorDetails from remix/route-pattern/href
MatchParamMeta from remix/route-pattern/match
Combine patterns
joinPatterns builds a new pattern from a base pattern.
import { joinPatterns } from 'remix/route-pattern/join'
let user = joinPatterns('users', ':id')
user.toString()
let apiUser = joinPatterns('api(/v:version)', '://remix.run/users/:id')
apiUser.toString()
- Protocol: if second pattern has a protocol, overrides base pattern
- Hostname: if second pattern has a hostname, overrides base pattern
- Port: if second pattern has a port, overrides base pattern
- Pathname: concatenates pathnames, adding a
/ in between as necessary
- Search constraints: merges search constraints by key
Benchmarks
Benchmarks live in bench/.
Related Work
License
See LICENSE