remix/route-pattern/join
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 remixQuick 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()
// /blog/:slug
match?.params
// { slug: 'v3' }
match?.data
// { name: 'blog-post' }
import { createHref } from 'remix/route-pattern/href'
createHref('blog/:slug', { slug: 'v3' })
// '/blog/v3'
createHref('api(/v:version)/*path', { version: '2', path: 'users/profile' })
// '/api/v2/users/profile'
createHref('http(s)://:region.cdn.com/assets/*file.:ext', {
region: 'us-west',
file: 'images/logo',
ext: 'png',
})
// 'https://us-west.cdn.com/assets/images/logo.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
Pattern syntax
Protocol
Protocol must be http, https, or http(s):
'https://example.com' // matches https://example.com
'http(s)://example.com' // matches http://example.com, https://example.comHostname & pathname
Variables capture dynamic segments using :name:
'users/:id' // matches /users/123
'blog/:year-:month-:day/:slug' // matches /blog/2024-01-15/helloWildcards match multi-segment paths using *name:
'files/*path' // matches /files/images/logo.png
'node_modules/*package/dist/index.js' // matches /node_modules/@remix-run/router/dist/index.js
'files/*' // matches any path under /files, but doesn't capture the wildcard valueOptionals make parts optional using ():
'api(/v:version)/users' // matches /api/users, /api/v2/users
'blog/:slug(.html)' // matches /blog/hello, /blog/hello.html
'docs(/guides/:category)' // matches /docs, /docs/guides/routing
'api(/v:major(.:minor))' // matches /api, /api/v2, /api/v2.1While variables, wilcards, and optionals are most prevalent in pathnames, you can also use them in hostnames:
':tenant.example.com/dashboard' // matches acme.example.com/dashboard
'(www.)example.com/blog/:slug(.html)' // matches example.com/blog/hello, www.example.com/blog/hello.html
'*.example.com/files/*path' // matches cdn.example.com/files/images/logo.png
'(:locale.)example.com/docs(/:section)' // matches en.example.com/docs, en.example.com/docs/guidesSearch
Search constraints narrow matches using ?key or ?key=value:
'search?q' // key must be present
'search?q=routing' // requires ?q=routing exactlyMatch 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
// Type safe params ^? { slug: string } | undefined
let docsMatcher = createMatcher('://(:tenant.)host.com/docs/*path.:ext')
docsMatcher.match(url)?.params
// Type safe params ^? { tenant: string | undefined, path: string, ext: string } | undefinedMatch 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>()
// Any data type you want! 👆
matcher.add('/', 'home')
matcher.add('blog/:slug', 'blog-post')
matcher.add('api(/v:version)/*path', 'api')
matcher.match('https://example.com/blog/v3')
// { params: { slug: 'v3' }, data: 'blog-post' }
matcher.match('https://example.com/api/v2/users/profile')
// { params: { version: '2', path: 'users/profile' }, data: 'api' }The matched pattern is only known at runtime, so matched params are not inferred when matching with createMultiMatcher.
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:
- 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.
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())
// ['/files/readme.md', '/files/:name', '/files/*path']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' })
// '/blog/v3'
createHref('api(/v:version)/*path', { path: 'users/profile' })
// '/api/users/profile'
createHref('api(/v:version)/*path', { version: '2', path: 'users/profile' })
// '/api/v2/users/profile'
createHref('http(s)://:region.cdn.com/assets/*file.:ext', {
region: 'us-west',
file: 'images/logo',
ext: 'png',
})
// 'https://us-west.cdn.com/assets/images/logo.png'
createHref('blog/:slug?ref=docs', { slug: 'v3' }, { utm_source: 'newsletter' })
// '/blog/v3?utm_source=newsletter&ref=docs'Note: optional groups without params are included in the generated href:
createHref('todos(/new)')
// '/todos/new'
createHref('products(.json)')
// '/products.json'Parse & stringify patterns
You can explicitly parse and stringify patterns:
import { RoutePattern } from 'remix/route-pattern'
let pattern = RoutePattern.parse('://example.com/blog/:slug')
// ^? RoutePattern
pattern.toString()
// '://example.com/blog/:slug'
pattern.toJSON()
// { hostname: 'example.com', pathname: 'blog/:slug', ... }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.
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()
// '/users/:id'
let apiUser = joinPatterns('api(/v:version)', '://remix.run/users/:id')
apiUser.toString()
// '://remix.run/api(/v:version)/users/:id'- 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