Type-safe URL matching and href generation for JavaScript. Supports path params, wildcards, optionals, and full-URL patterns with predictable ranking.
Installation
Imports
import { RoutePattern, ArrayMatcher, TrieMatcher } from 'remix/route-pattern'
import { compare, ascending, descending } from 'remix/route-pattern/specificity'
import type { Params, Join, Match, Matcher } from 'remix/route-pattern'
RoutePattern
A class for matching and generating URLs based on a defined pattern.
class RoutePattern<source extends string = string>
Constructor
new RoutePattern(source: source)
The pattern string to parse. Supports variables (:name), wildcards (*name), optionals (()), and search params (?key=value).
let pattern = new RoutePattern('blog/:slug')
let api = new RoutePattern('api(/v:version)/*path')
let cdn = new RoutePattern('http(s)://:region.cdn.com/assets/*file.:ext')
let search = new RoutePattern('search?q')
Properties
The original pattern string.
The pathname portion of the pattern.
The protocol portion of the pattern (e.g., ‘http’, ‘https’, ‘http(s)’, or ”).
The hostname portion of the pattern.
The port portion of the pattern.
The search portion of the pattern.
Methods
pattern.match()
Match a URL against this pattern.
match(
url: string | URL,
options?: { ignoreCase?: boolean }
): RoutePatternMatch<source> | null
The URL to match against the pattern.
When true, pathname matching is case-insensitive. Hostname is always case-insensitive; search remains case-sensitive.
match
RoutePatternMatch<source> | null
The match result, or null if no match.
let pattern = new RoutePattern('blog/:slug')
let match = pattern.match('https://remix.run/blog/hello')
if (match) {
console.log(match.params.slug) // 'hello'
console.log(match.url.pathname) // '/blog/hello'
}
pattern.test()
Test whether a URL matches this pattern.
test(url: string | URL): boolean
true if the URL matches the pattern, false otherwise.
let pattern = new RoutePattern('blog/:slug')
pattern.test('https://remix.run/blog/hello') // true
pattern.test('https://remix.run/docs') // false
pattern.href()
Generate a URL href from this pattern using the given parameters.
href(...args: HrefArgs<source>): string
Parameters to use for building the href. The exact signature depends on whether the pattern has required or optional parameters.
let pattern = new RoutePattern('blog/:slug')
pattern.href({ slug: 'hello' }) // '/blog/hello'
let api = new RoutePattern('api(/v:version)/*path')
api.href({ version: '2', path: 'users/profile' }) // '/api/v2/users/profile'
api.href({ path: 'users/profile' }) // '/api/users/profile'
let search = new RoutePattern('search?q')
search.href({}, { q: 'typescript', sort: 'date' }) // '/search?q=typescript&sort=date'
Throws: HrefError if required parameters are missing or if the pattern requires a hostname but none is provided.
pattern.join()
Join this pattern with another pattern to create a new pattern.
join<other extends string>(
other: other | RoutePattern<other>
): RoutePattern<Join<source, other>>
other
string | RoutePattern
required
The pattern to join with this one.
pattern
RoutePattern<Join<source, other>>
A new pattern that represents the joined result.
let base = new RoutePattern('api/v2')
let users = base.join('users/:id')
users.source // 'api/v2/users/:id'
let nested = users.join('posts/:postId')
nested.source // 'api/v2/users/:id/posts/:postId'
pattern.toString()
Convert the pattern to a string.
The pattern source string.
let pattern = new RoutePattern('blog/:slug')
pattern.toString() // 'blog/:slug'
RoutePatternMatch
The result of matching a URL against a pattern.
type RoutePatternMatch<source extends string = string> = {
pattern: RoutePattern
url: URL
params: Params<source>
paramsMeta: {
hostname: PartPatternMatch
pathname: PartPatternMatch
}
}
The pattern that matched.
The URL that was matched.
The parsed parameters from the URL. Fully typed based on the pattern.
Rich information about matched params (variables and wildcards) in the hostname and pathname, analogous to RegExp groups/indices.
Matcher
A type for matching URLs against multiple patterns.
interface Matcher<data = unknown> {
readonly ignoreCase: boolean
add(pattern: string | RoutePattern, data: data): void
match(url: string | URL, compareFn?: CompareFn): Match<string, data> | null
matchAll(url: string | URL, compareFn?: CompareFn): Array<Match<string, data>>
}
Properties
When true, pathname matching is case-insensitive for all patterns in this matcher.
Methods
matcher.add()
Add a pattern to the matcher.
add(pattern: string | RoutePattern, data: data): void
pattern
string | RoutePattern
required
The pattern to add.
The data to associate with the pattern. Can be any type.
matcher.match()
Find the best match for a URL.
match(
url: string | URL,
compareFn?: CompareFn
): Match<string, data> | null
Comparison function for determining which match is best. Defaults to specificity-based ranking.
match
Match<string, data> | null
The match result, or null if no match was found.
matcher.matchAll()
Find all matches for a URL.
matchAll(
url: string | URL,
compareFn?: CompareFn
): Array<Match<string, data>>
Comparison function for sorting matches. Defaults to specificity-based ranking.
matches
Array<Match<string, data>>
All matches, sorted by the comparison function.
ArrayMatcher
A simple matcher implementation that checks patterns sequentially. Best for small apps (~80 routes or fewer).
class ArrayMatcher<data> implements Matcher<data>
Constructor
new ArrayMatcher(options?: { ignoreCase?: boolean })
When true, pathname matching is case-insensitive for all patterns.
Example
import { ArrayMatcher } from 'remix/route-pattern'
let matcher = new ArrayMatcher<string>()
matcher.add('/', 'home')
matcher.add('blog/:slug', 'blog-post')
matcher.add('api/*path', 'api')
let match = matcher.match('https://remix.run/blog/hello')
// {
// pattern: RoutePattern<'blog/:slug'>,
// params: { slug: 'hello' },
// data: 'blog-post',
// ...
// }
TrieMatcher
A trie-based matcher implementation for efficient matching of large route sets. Best for large apps (hundreds of routes).
class TrieMatcher<data> implements Matcher<data>
Constructor
new TrieMatcher(options?: { ignoreCase?: boolean })
When true, pathname matching is case-insensitive for all patterns.
Example
import { TrieMatcher } from 'remix/route-pattern'
// Same API as ArrayMatcher, but with trie-based matching
let matcher = new TrieMatcher<string>()
matcher.add('/', 'home')
matcher.add('blog/:slug', 'blog-post')
matcher.add('api/*path', 'api')
let match = matcher.match('https://remix.run/blog/hello')
ArrayMatcher vs TrieMatcher: ArrayMatcher is best for small apps (~80 routes or fewer), while TrieMatcher is best for large apps (hundreds of routes). Performance depends on your specific patterns—benchmark both to verify which is faster for your app.
Specificity Functions
Functions for comparing and sorting route pattern matches by specificity.
compare()
Compare two matches by specificity.
function compare(
a: RoutePatternMatch,
b: RoutePatternMatch
): -1 | 0 | 1
a
RoutePatternMatch
required
The first match to compare.
b
RoutePatternMatch
required
The second match to compare.
-1 if a is less specific, 1 if a is more specific, 0 if tied.
import { compare } from 'remix/route-pattern/specificity'
let matches = [
pattern1.match(url),
pattern2.match(url)
].filter(Boolean)
matches.sort(compare) // Sort from least to most specific
ascending()
Comparator function for sorting matches from least specific to most specific.
function ascending(
a: RoutePatternMatch,
b: RoutePatternMatch
): number
import { ascending } from 'remix/route-pattern/specificity'
matches.sort(ascending)
descending()
Comparator function for sorting matches from most specific to least specific.
function descending(
a: RoutePatternMatch,
b: RoutePatternMatch
): number
import { descending } from 'remix/route-pattern/specificity'
matches.sort(descending) // Most specific first
lessThan()
Returns true if match a is less specific than match b.
function lessThan(
a: RoutePatternMatch,
b: RoutePatternMatch
): boolean
greaterThan()
Returns true if match a is more specific than match b.
function greaterThan(
a: RoutePatternMatch,
b: RoutePatternMatch
): boolean
equal()
Returns true if matches a and b have equal specificity.
function equal(
a: RoutePatternMatch,
b: RoutePatternMatch
): boolean
Error Classes
ParseError
Thrown when a pattern string cannot be parsed.
class ParseError extends Error {
type: 'unmatched (' | 'unmatched )' | 'missing variable name' | 'dangling escape' | 'invalid protocol'
source: string
index: number
}
The pattern string that failed to parse.
The character index where the error occurred.
try {
new RoutePattern('users/:id)')
} catch (error) {
if (error instanceof ParseError) {
console.log(error.type) // 'unmatched )'
console.log(error.index) // 10
}
}
HrefError
Thrown when href generation fails due to missing parameters or invalid pattern structure.
class HrefError extends Error {
details: HrefErrorDetails
}
Detailed information about why href generation failed.
try {
let pattern = new RoutePattern('blog/:slug')
pattern.href({}) // Missing required param 'slug'
} catch (error) {
if (error instanceof HrefError) {
console.log(error.details.type) // 'missing-params'
console.log(error.details.missingParams) // ['slug']
}
}
Types
Params
Infer the parameters from a pattern string.
type Params<T extends string>
import type { Params } from 'remix/route-pattern'
type BlogParams = Params<'blog/:slug'>
// { slug: string }
type ApiParams = Params<'api(/v:version)/*path'>
// { version: string | undefined, path: string }
type FullUrlParams = Params<'http(s)://:region.cdn.com/assets/*file.:ext'>
// { region: string, file: string, ext: string }
Join
Join two pattern strings together at the type level.
type Join<A extends string, B extends string>
import type { Join } from 'remix/route-pattern'
type Result = Join<'api/v2', 'users/:id'>
// 'api/v2/users/:id'
type Nested = Join<'blog', ':year/:month/:slug'>
// 'blog/:year/:month/:slug'
Match
A match result that includes associated data.
type Match<source extends string = string, data = unknown> = RoutePatternMatch<source> & {
data: data
}
The data that was associated with the pattern when it was added to the matcher.
Pattern Syntax
Variables
Capture dynamic segments using :name.
new RoutePattern('users/:id')
// matches: /users/123
// params: { id: '123' }
new RoutePattern('blog/:year-:month-:day/:slug')
// matches: /blog/2024-01-15/hello
// params: { year: '2024', month: '01', day: '15', slug: 'hello' }
Wildcards
Match multi-segment paths using *name.
new RoutePattern('files/*path')
// matches: /files/images/logo.png
// params: { path: 'images/logo.png' }
new RoutePattern('node_modules/*package/dist/index.js')
// matches: /node_modules/@remix-run/router/dist/index.js
// params: { package: '@remix-run/router' }
new RoutePattern('files/*')
// matches: /files/anything/here
// params: { '*': 'anything/here' }
Optionals
Make parts optional using ().
new RoutePattern('api(/v:version)/users')
// matches: /api/users AND /api/v2/users
// params: { version: undefined } OR { version: '2' }
new RoutePattern('blog/:slug(.html)')
// matches: /blog/hello AND /blog/hello.html
new RoutePattern('docs(/guides/:category)')
// matches: /docs AND /docs/guides/routing
new RoutePattern('api(/v:major(.:minor))')
// nested: /api, /api/v2, /api/v2.1
Search Parameters
Narrow matches using query string constraints.
new RoutePattern('search?q')
// requires ?q in URL (any value or empty)
new RoutePattern('search?q=')
// requires ?q with a value
new RoutePattern('search?q=routing')
// requires ?q=routing exactly
new RoutePattern('search?q=x&q=y')
// requires both values
Full URL Patterns
Match protocol, hostname, port, pathname, and search.
new RoutePattern('https://api.example.com/users/:id')
// Only matches https://api.example.com URLs
new RoutePattern('://example.com/api')
// Matches any protocol on example.com
new RoutePattern('http(s)://:subdomain.example.com/*path')
// Matches http or https on any subdomain
new RoutePattern(':3000/api')
// Matches port 3000 on any host
Specificity Rules
When multiple patterns match a URL, the most specific pattern wins.
Pathname Specificity
Static segments beat variables, and variables beat wildcards at each position (evaluated left-to-right).
import { ArrayMatcher } from 'remix/route-pattern'
let matcher = new ArrayMatcher<string>()
matcher.add('blog/hello', 'static')
matcher.add('blog/:slug', 'variable')
matcher.add('blog/*path', 'wildcard')
matcher.add('*path', 'catch-all')
matcher.match('https://example.com/blog/hello')
// { data: 'static' }
// 'blog/hello' wins: static beats variable/wildcard
Search Specificity
More constrained search params = more specific.
let matcher = new ArrayMatcher<string>()
matcher.add('search', 'no-params')
matcher.add('search?q', 'has-q')
matcher.add('search?q=', 'has-q-with-value')
matcher.add('search?q=hello', 'exact-match')
matcher.match('https://example.com/search?q=hello')
// { data: 'exact-match' }
Hostname Specificity
Similar to pathname: static beats variables, variables beat wildcards (evaluated right-to-left for hostname).
let matcher = new ArrayMatcher<string>()
matcher.add('://api.example.com/users', 'static-host')
matcher.add('://:subdomain.example.com/users', 'variable-host')
matcher.add('://*/users', 'wildcard-host')
matcher.match('https://api.example.com/users')
// { data: 'static-host' }