Skip to main content
Type-safe URL matching and href generation for JavaScript. Supports path params, wildcards, optionals, and full-URL patterns with predictable ranking.

Installation

npm i remix

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)
source
string
required
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

source
string
The original pattern string.
pathname
string
The pathname portion of the pattern.
protocol
string
The protocol portion of the pattern (e.g., ‘http’, ‘https’, ‘http(s)’, or ”).
hostname
string
The hostname portion of the pattern.
port
string
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
url
string | URL
required
The URL to match against the pattern.
options
object
options.ignoreCase
boolean
default:false
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
url
string | URL
required
The URL to test.
matches
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
args
HrefArgs<source>
required
Parameters to use for building the href. The exact signature depends on whether the pattern has required or optional parameters.
href
string
The generated URL href.
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.
toString(): string
source
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
  }
}
pattern
RoutePattern
The pattern that matched.
url
URL
The URL that was matched.
params
Params<source>
The parsed parameters from the URL. Fully typed based on the pattern.
paramsMeta
object
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

ignoreCase
boolean
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.
data
data
required
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
url
string | URL
required
The URL to match.
compareFn
CompareFn
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>>
url
string | URL
required
The URL to match.
compareFn
CompareFn
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 })
options
object
options.ignoreCase
boolean
default:false
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 })
options
object
options.ignoreCase
boolean
default:false
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.
result
-1 | 0 | 1
-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
}
type
string
The type of parse error.
source
string
The pattern string that failed to parse.
index
number
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
}
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
}
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' }

Build docs developers (and LLMs) love