
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
@remix-run/route-pattern
Advanced tools
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.
npm i remix
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'
| Import | Description |
|---|---|
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
Examples in this README use remix/route-pattern/* imports. The same APIs are also available from the direct package entrypoints: @remix-run/route-pattern, @remix-run/route-pattern/href, @remix-run/route-pattern/match, @remix-run/route-pattern/join, and @remix-run/route-pattern/specificity.
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
Variables capture dynamic segments using :name:
'users/:id' // matches /users/123
'blog/:date/:slug' // matches /blog/2024-01-15/hello
'files/:name.:ext' // matches /files/readme.md
Pathname variables possessively capture the largest non-empty run up to / or .. Hyphens are data, so UUIDs and slugs remain intact. A variable may have static text before it, but every path through following optionals must reach /, ., a wildcard, or the end of the hostname or pathname. Capture an inseparable value such as a date with one variable instead of :year-:month-:day.
Raw / and . are structural delimiters. Their percent-encoded forms remain data and are decoded in the resulting param. Static pattern text may use either decoded text or percent encoding, so /café and /caf%C3%A9 match the same pathname text.
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
Patterns may contain any number of wildcards when static text or a delimiter separates them. Adjacent wildcards such as *left*right are rejected because their capture boundary is ambiguous.
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
Optionals compile as state branches rather than concrete variants, so independent and nested optionals do not cause exponential matcher construction. Empty optionals and adjacent optional branches that give the same URL different capture schemas are rejected.
While variables, wildcards, 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
Capture names may repeat. params uses the last participating capture in pattern order, while paramsMeta retains every participating capture:
let matcher = createMatcher('/:id/:id')
let match = matcher.match('https://example.com/first/second')
match?.params
// { id: 'second' }
match?.paramsMeta.pathname.map(({ name, value }) => ({ name, value }))
// [{ name: 'id', value: 'first' }, { name: 'id', value: 'second' }]
Escape characters with \:
'time/12\\:30' // matches /time/12:30
'calculator/2\\*3' // matches /calculator/2*3
'wiki/Mercury_\\(planet\\)' // matches /wiki/Mercury_(planet)
'wiki/AC\\/DC' // matches /wiki/AC%2FDC
Search constraints narrow matches using ?key or ?key=value:
'search?q' // key must be present
'search?q=routing' // requires ?q=routing exactly
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
Matchers accept absolute URL strings or URL objects. To match a relative URL reference, pass an absolute baseURL; the input is resolved with the same semantics as new URL(input, baseURL), and the resolved URL is returned on the match.
let match = blogMatcher.match('../blog/v3', {
baseURL: 'https://example.com/admin/settings',
})
match?.params
// { slug: 'v3' }
match?.url.href
// 'https://example.com/blog/v3'
This works for root-relative, path-relative, query-relative, and network-path references. Without baseURL, string inputs must still be absolute.
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.
Each match returns:
url: the URL object that was matchedpattern: the matched RoutePatterndata: the data attached with matcher.add(pattern, data)params: captured param valuesparamsMeta: hostname and pathname param metadataparamsMeta.hostname and paramsMeta.pathname are arrays of { type, name, value, begin, end } entries. The offsets are measured after URL normalization. A pattern with no hostname matches any hostname, represented in paramsMeta.hostname as an unnamed wildcard entry.
Set ignoreCase: true to make pathname matching case-insensitive. Hostname matching is always case-insensitive, and search constraints are always case-sensitive.
let matcher = createMatcher('/Docs/:slug', { ignoreCase: true })
matcher.match('https://example.com/docs/Intro')?.params
// { slug: 'Intro' }
Matchers limit individual pattern size, total matcher size, and the work performed by one match, including variable scans and comparisons between wildcard or optional captures. Pattern and matcher sizes are measured in UTF-8 bytes. Direct package consumers may lower or raise individual limits. Exceeding one throws MatcherResourceError with structured details instead of silently abandoning matching:
let matcher = createMultiMatcher({
limits: { maxPatternSize: 4096, maxMatchWork: 100_000 },
})
When multiple patterns match the same URL, route-pattern chooses the most specific match deterministically. Matches are ranked left-to-right, character-by-character:
This is the same ranking used by createMultiMatcher.
For advanced use cases, /specificity provides comparison utilities: lessThan, greaterThan, equal, descending, ascending, compare. lessThan(a, b) returns true when match a is less specific than match b. 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', null)
let matches = matcher.matchAll('https://example.com/files/readme')
matches.sort(descending).map((match) => match.pattern.toString())
// ['/files/readme', '/files/:name', '/files/*path']
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' },
{
searchParams: { utm_source: 'newsletter' },
},
)
// '/blog/v3?utm_source=newsletter&ref=docs'
createHref('users/:id', { id: 'a.b' })
// '/users/a%2Eb' (the encoded dot remains variable data when matched)
Pass baseURL to generate a path-relative reference to a same-origin route. Patterns with a different origin remain absolute.
let baseURL = new URL('https://example.com/admin/settings')
createHref('users/:id', { id: '123' }, { baseURL })
// '../users/123'
createHref('https://cdn.example.com/assets/*path', { path: 'logo.svg' }, { baseURL })
// 'https://cdn.example.com/assets/logo.svg'
The searchParams option accepts a plain object or URLSearchParams. Use URLSearchParams when duplicate keys or their order matter:
let searchParams = new URLSearchParams([
['tag', 'featured'],
['tag', 'popular'],
])
createHref('search', undefined, { searchParams })
// '/search?tag=featured&tag=popular'
createHref() throws CreateHrefError when it cannot safely generate an href. The error exposes stable structured details on error.details; the string message is for humans.
Common failures include missing required params, nameless wildcards, invalid hostname params, empty pathname variables, and origin patterns that specify a protocol or port without a concrete hostname.
Hostname params reject URL structural characters, including backslashes. Hostname variables also reject dots; hostname wildcards allow dots to span labels.
Note: optional groups without params are included in the generated href:
createHref('todos(/new)')
// '/todos/new'
createHref('products(.json)')
// '/products.json'
You can explicitly parse and stringify patterns. Create a RoutePattern with RoutePattern.parse and use the methods and helpers below instead of reading parsed token internals.
import { getRoutePatternCaptures, RoutePattern } from 'remix/route-pattern'
let pattern = RoutePattern.parse('://:tenant.example.com/blog/:slug(/*path)')
// ^? RoutePattern
pattern.toString()
// '://:tenant.example.com/blog/:slug(/*path)'
pattern.toJSON()
// { hostname: ':tenant.example.com', pathname: 'blog/:slug(/*path)', ... }
getRoutePatternCaptures(pattern)
// [
// { part: 'hostname', type: ':', name: 'tenant', optional: false },
// { part: 'pathname', type: ':', name: 'slug', optional: false },
// { part: 'pathname', type: '*', name: 'path', optional: true },
// ]
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.
RoutePattern.toJSON() returns a RoutePatternJSON object with serialized protocol, hostname, port, pathname, and search fields. RoutePattern.parse() throws ParseError for malformed sources; the error exposes stable type, source, and index fields.
The public support types are:
RoutePatternCapture from remix/route-patternRoutePatternJSON from remix/route-patternCreateHrefErrorDetails from remix/route-pattern/hrefCreateHrefOptions and CreateHrefSearchParams from remix/route-pattern/hrefMatchParamMeta from remix/route-pattern/matchMatchOptions from remix/route-pattern/matchMatcherLimits from remix/route-pattern/matchMatcherResourceError and MatcherResourceErrorDetails from remix/route-pattern/matchLiteral patterns are validated and infer named params until the type-level parser reaches its 64-step complexity budget. Larger runtime-valid patterns remain accepted and fall back to safe general pattern types instead of risking a TypeScript excessive-instantiation error.
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'
/ in between as necessaryBenchmarks live in bench/.
See LICENSE
FAQs
Match and generate URLs with strong typing
The npm package @remix-run/route-pattern receives a total of 176,920 weekly downloads. As such, @remix-run/route-pattern popularity was classified as popular.
We found that @remix-run/route-pattern demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.