Remix
Remix

route-pattern

Type-safe URL matching and href generation for JavaScript. route-pattern supports path params, wildcards, optionals, and full-URL patterns with predictable ranking.

Features

  • Type-Safe Params - Infer params from patterns for compile-time route correctness
  • Flexible Pattern Syntax - Variables, wildcards, optionals, and query constraints
  • Full URL Support - Match protocol, host, pathname, and search params
  • Deterministic Ranking - Static segments beat params, and params beat wildcards
  • Runtime Agnostic - Works across Node.js, Bun, Deno, Cloudflare Workers, and browsers

Installation

npm i remix

Quick Example

import { RoutePattern } from 'remix/route-pattern'

let blog = new RoutePattern('blog/:slug')
blog.match('https://remix.run/blog/v3') // { params: { slug: 'v3' } }
blog.href({ slug: 'v3' }) // '/blog/v3'

let api = new RoutePattern('api(/v:version)/*path')
api.match('https://api.com/api/v2/users/profile') // { params: { version: '2', path: 'users/profile' } }
api.href({ version: '2', path: 'users/profile' }) // '/api/v2/users/profile'
api.href({ path: 'users/profile' }) // '/api/users/profile'

let cdn = new RoutePattern('http(s)://:region.cdn.com/assets/*file.:ext')
cdn.match('https://us-west.cdn.com/assets/images/logo.png') // { params: { region: 'us-west', file: 'images/logo', ext: 'png' } }
cdn.href({ region: 'us-west', file: 'images/logo', ext: 'png' }) // 'https://us-west.cdn.com/assets/images/logo.png'

Intuitive syntax

Variables capture dynamic segments using :name:

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

Wildcards match multi-segment paths using *name:

new RoutePattern('files/*path') // matches /files/images/logo.png
new RoutePattern('node_modules/*package/dist/index.js') // matches /node_modules/@remix-run/router/dist/index.js
new RoutePattern('files/*') // matches any path under /files, but doesn't capture the value for the wildcard

Optionals make parts optional using ():

new RoutePattern('api(/v:version)/users') // matches /api/users AND /api/v2/users
new RoutePattern('blog/:slug(.html)') // matches /blog/hello AND /blog/hello.html
new RoutePattern('docs(/guides/:category)') // multiple segments optional: /docs OR /docs/guides/routing
new RoutePattern('api(/v:major(.:minor))') // nested optionals: /api, /api/v2, /api/v2.1

Search params narrow matches using ?key, ?key=, or ?key=value. Parsing and serialization follow URLSearchParams (application/x-www-form-urlencoded): ?key and ?key= are the same constraint (stored as an empty Set in ast.search: key must be present; empty value is OK), and spaces use + / %20 like in real query strings.

new RoutePattern('search?q') // same constraint as ?q= — key must be present
new RoutePattern('search?q=routing') // requires ?q=routing exactly

Flexible matching for partial URL patterns:

new RoutePattern('blog/:slug') // omits protocol/hostname, matches any origin
new RoutePattern('://example.com/api') // omits protocol, matches http and https
new RoutePattern('search?q') // allows additional search params beyond ?q

Matchers

Match URLs against multiple patterns. Each pattern can have associated data (handlers, route IDs, metadata, etc.):

import { createMatcher } from 'remix/route-pattern'

// Any data type you want!  👇
let matcher = createMatcher<string>()

matcher.add('/', 'home')
matcher.add('blog/:slug', 'blog-post')
matcher.add('api(/v:version)/*path', 'api')

matcher.match('https://example.com/blog/v3')
// { pattern: 'blog/:slug', params: { slug: 'v3' }, data: 'blog-post' }

matcher.match('https://example.com/api/v2/users/profile')
// { pattern: 'api(/v:version)/*path', params: { version: '2', path: 'users/profile' }, data: 'api' }

Specificity

When multiple patterns match a URL, the most specific pattern wins.

Pathname specificity (left-to-right):

import { createMatcher } from 'remix/route-pattern'

let matcher = createMatcher<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')
// { pattern: 'blog/hello', params: {}, data: 'static' }
// 'blog/hello' wins: static segments beat variables/wildcards at each position

Search parameter specificity:

let matcher = createMatcher<string>()
matcher.add('search', 'no-params')
matcher.add('search?q', 'has-q')
matcher.add('search?q=hello', 'exact-match')

matcher.match('https://example.com/search?q=hello')
// { pattern: 'search?q=hello', params: {}, data: 'exact-match' }
// More constrained search params = more specific (`?q` and `?q=` tie)

Benchmark

Benchmarks live in bench/.

Related Work

License

See LICENSE