Tutorial: Working with URL Patterns
In this tutorial, you will learn how to use the route-pattern package to match URLs against patterns, extract parameters, generate URLs, and compare pattern specificity. These are the building blocks that power Remix's router under the hood.
Prerequisites
You should be comfortable with JavaScript. No server programming experience is needed.
Step 1: Parse and Match a Pattern
A RoutePattern is created from a pattern string. Once created, you can match URLs against it.
import { RoutePattern } from 'remix/route-pattern'
// Create a pattern with a dynamic segment
let pattern = new RoutePattern('/products/:id')
// Match a URL
let match = pattern.match(new URL('http://localhost/products/42'))
if (match) {
console.log(match.params) // { id: "42" }
}
// Non-matching URLs return null
let noMatch = pattern.match(new URL('http://localhost/about'))
console.log(noMatch) // nullWhat is a Dynamic Segment?
A dynamic segment is a part of the URL pattern that starts with : (like :id). It acts as a placeholder that matches any single path segment. When a URL matches, the actual value is captured in params.
Step 2: Use Different Segment Types
Patterns support four kinds of segments. Let's try each one:
import { RoutePattern } from 'remix/route-pattern'
// Static -- matches exactly
let about = new RoutePattern('/about')
about.test(new URL('http://localhost/about')) // true
about.test(new URL('http://localhost/contact')) // false
// Dynamic -- matches one segment
let user = new RoutePattern('/users/:id')
user.match(new URL('http://localhost/users/alice'))!.params
// { id: "alice" }
// Wildcard -- matches everything after the prefix
let files = new RoutePattern('/files/*path')
files.match(new URL('http://localhost/files/docs/readme.md'))!.params
// { path: "docs/readme.md" }
// Optional -- the segment may or may not be present
let lang = new RoutePattern('/docs/(:lang)')
lang.match(new URL('http://localhost/docs'))!.params
// { lang: undefined }
lang.match(new URL('http://localhost/docs/en'))!.params
// { lang: "en" }Step 3: Generate URLs with href()
Instead of building URL strings by hand, let the pattern do it for you:
import { RoutePattern } from 'remix/route-pattern'
let pattern = new RoutePattern('/users/:userId/posts/:postId')
// Generate a URL by providing the required params
let url = pattern.href({ userId: '5', postId: '99' })
console.log(url) // "/users/5/posts/99"
// Static patterns need no params
let home = new RoutePattern('/')
home.href() // "/"
// You can also add query parameters (second argument)
let search = new RoutePattern('/search')
search.href({}, { q: 'remix', page: '2' })
// "/search?q=remix&page=2"If you forget a required parameter, href() throws a HrefError at runtime. With TypeScript, you get a compile-time error instead.
Step 4: Join Patterns Together
The join() method combines two patterns into one. This is useful when you have a base path and want to append more specific patterns:
import { RoutePattern } from 'remix/route-pattern'
let base = new RoutePattern('/api/v1')
let endpoint = new RoutePattern('/users/:id')
let joined = base.join(endpoint)
console.log(joined.source) // "/api/v1/users/:id"
joined.href({ id: '42' }) // "/api/v1/users/42"Step 5: Compare Specificity
When multiple patterns match the same URL, you need to decide which one should win. Specificity is the set of rules that determines priority.
The rules are:
- Static segments beat dynamic segments (
/users/newbeats/users/:id) - Dynamic segments beat wildcards (
/files/:namebeats/files/*path) - Longer static matches beat shorter ones
- More search constraints beat fewer
import { RoutePattern } from 'remix/route-pattern'
import {
compare,
ascending,
greaterThan,
} from 'remix/route-pattern/specificity'
let url = new URL('http://localhost/users/new')
let staticPattern = new RoutePattern('/users/new')
let dynamicPattern = new RoutePattern('/users/:id')
let wildcardPattern = new RoutePattern('/users/*rest')
let staticMatch = staticPattern.match(url)!
let dynamicMatch = dynamicPattern.match(url)!
let wildcardMatch = wildcardPattern.match(url)!
// Compare two matches directly
greaterThan(staticMatch, dynamicMatch) // true (static wins)
greaterThan(dynamicMatch, wildcardMatch) // true (dynamic wins)
// Sort an array of matches (least specific first)
let sorted = [wildcardMatch, staticMatch, dynamicMatch].sort(ascending)
console.log(sorted.map((m) => m.pattern.source))
// ["/users/*rest", "/users/:id", "/users/new"]Step 6: Use Matchers for Multiple Patterns
When you have many patterns, a matcher stores them all and finds the best match efficiently. The route-pattern package provides two implementations.
ArrayMatcher
Tests patterns one by one. Simple and works well for small numbers of routes:
import { ArrayMatcher, RoutePattern } from 'remix/route-pattern'
let matcher = new ArrayMatcher()
// Add patterns with associated data (any value you want)
matcher.add(new RoutePattern('/'), { name: 'home' })
matcher.add(new RoutePattern('/users'), { name: 'userList' })
matcher.add(new RoutePattern('/users/:id'), { name: 'userShow' })
matcher.add(new RoutePattern('/users/new'), { name: 'userNew' })
// Find the best match
let match = matcher.match(new URL('http://localhost/users/new'))
console.log(match.data) // { name: "userNew" } (static wins)
console.log(match.params) // {}
// Find ALL matches, sorted by specificity
let all = matcher.matchAll(new URL('http://localhost/users/new'))
console.log(all.map((m) => m.data.name))
// ["userNew", "userShow"] -- most specific firstTrieMatcher
Uses a trie (a tree-shaped data structure) for faster lookups. The API is identical to ArrayMatcher:
import { TrieMatcher, RoutePattern } from 'remix/route-pattern'
let matcher = new TrieMatcher()
// Same API as ArrayMatcher
matcher.add(new RoutePattern('/users'), { handler: listUsers })
matcher.add(new RoutePattern('/users/:id'), { handler: showUser })
let match = matcher.match(new URL('http://localhost/users/42'))
console.log(match.params) // { id: "42" }Which Matcher Should I Use?
For most applications, ArrayMatcher (the default in Remix's router) is perfectly fine. Switch to TrieMatcher if you have hundreds or thousands of routes and notice performance issues during matching.
Step 7: Handle Parse Errors
If you pass an invalid pattern string, the constructor throws a ParseError:
import { RoutePattern, ParseError } from 'remix/route-pattern'
try {
new RoutePattern('/users/:/oops')
} catch (error) {
if (error instanceof ParseError) {
console.error('Invalid pattern:', error.message)
}
}This is useful when patterns come from user input or configuration files that might contain mistakes.
What You Learned
RoutePatternparses pattern strings and matches them against URLs.- Dynamic segments (
:param), wildcards (*name), and optional segments ((:param)) capture different kinds of values. href()generates URLs from patterns and parameters.join()combines patterns for building prefixed routes.- Specificity determines which pattern wins when multiple patterns match.
ArrayMatcherandTrieMatcherstore multiple patterns and find the best match.
Next Steps
- API Reference -- Complete documentation of every class, method, and type.
- fetch-router Overview -- See how the router uses
route-patterninternally.