Remix
Remix

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 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()
// /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.com

Hostname & pathname

Variables capture dynamic segments using :name:

'users/:id' // matches /users/123
'blog/:year-:month-:day/:slug' // matches /blog/2024-01-15/hello

Wildcards 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 value

Optionals 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.1

While 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/guides

Search

Search constraints narrow matches using ?key or ?key=value:

'search?q' // key must be present
'search?q=routing' // requires ?q=routing exactly

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
// 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 } | undefined

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>()
// 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