Route Matching
How EreoJS matches URLs to route handlers.
Import
import {
// Factory and class
createMatcher,
RouteMatcher,
// Matching functions
matchRoute,
matchWithLayouts,
// Utilities
parsePathSegments,
calculateRouteScore,
patternToRegex
} from '@ereo/router'createMatcher
Factory function to create a route matcher from routes.
function createMatcher(routes: Route[]): RouteMatcherconst routes = await router.getRoutes()
const matcher = createMatcher(routes)
const match = matcher.match('/posts/123')RouteMatcher Class
Pre-compiles routes for efficient matching.
Constructor
const matcher = new RouteMatcher(routes)Methods
match()
Match a URL pathname against compiled routes. Returns the first matching route.
match(pathname: string): RouteMatch | nullconst match = matcher.match('/posts/hello-world')
if (match) {
console.log(match.route) // Route object
console.log(match.params) // { slug: 'hello-world' }
console.log(match.pathname) // '/posts/hello-world'
}getRoutes()
Get all routes in the matcher.
getRoutes(): Route[]const allRoutes = matcher.getRoutes()
console.log(`Matcher has ${allRoutes.length} routes`)addRoute()
Add a route dynamically. Maintains sorted order by score.
addRoute(route: Route): voidmatcher.addRoute({
id: '/api/custom',
path: '/api/custom',
file: '/path/to/handler.ts'
})removeRoute()
Remove a route by ID.
removeRoute(routeId: string): booleanconst removed = matcher.removeRoute('/api/custom')
console.log('Removed:', removed) // true if found and removedmatchRoute
Match a URL path against a single route with pre-parsed segments.
function matchRoute(
pathname: string,
route: Route,
segments: RouteSegment[]
): RouteMatch | nullconst segments = parsePathSegments('/posts/[slug]')
const match = matchRoute('/posts/hello-world', route, segments)matchWithLayouts
Match with layout resolution. Returns all matching layouts from root to the matched route.
function matchWithLayouts(
pathname: string,
routes: Route[]
): MatchResult | nullinterface MatchResult {
route: Route
params: RouteParams
pathname: string
layouts: Route[] // From outermost to innermost
}const result = matchWithLayouts('/blog/posts/hello', routes)
if (result) {
console.log(result.route) // The matched route
console.log(result.params) // { slug: 'hello' }
console.log(result.layouts) // [rootLayout, blogLayout]
}Utility Functions
parsePathSegments
Parse a path pattern into segments for analysis.
function parsePathSegments(path: string): RouteSegment[]interface RouteSegment {
raw: string
type: 'static' | 'dynamic' | 'catchAll' | 'optional'
paramName?: string
}const segments = parsePathSegments('/posts/[slug]/comments')
// [
// { raw: 'posts', type: 'static' },
// { raw: '[slug]', type: 'dynamic', paramName: 'slug' },
// { raw: 'comments', type: 'static' }
// ]
const catchAll = parsePathSegments('/docs/[...path]')
// [
// { raw: 'docs', type: 'static' },
// { raw: '[...path]', type: 'catchAll', paramName: 'path' }
// ]
const optional = parsePathSegments('/posts/[[page]]')
// [
// { raw: 'posts', type: 'static' },
// { raw: '[[page]]', type: 'optional', paramName: 'page' }
// ]calculateRouteScore
Calculate route score for sorting. Higher scores are matched first (more specific routes).
function calculateRouteScore(segments: RouteSegment[]): numberconst staticScore = calculateRouteScore(parsePathSegments('/about'))
const dynamicScore = calculateRouteScore(parsePathSegments('/posts/[id]'))
const catchAllScore = calculateRouteScore(parsePathSegments('/docs/[...path]'))
// staticScore > dynamicScore > catchAllScoreRoute scores by segment type:
- Static: 100 points
- Dynamic: 50 points
- Optional: 30 points
- Catch-all: 10 points
Earlier segments have more weight (multiplier decreases with position).
patternToRegex
Convert route pattern segments to a regular expression.
function patternToRegex(segments: RouteSegment[]): RegExpconst segments = parsePathSegments('/posts/[slug]')
const regex = patternToRegex(segments)
regex.test('/posts/hello') // true
regex.test('/posts/') // false
regex.test('/posts/a/b') // falseMatching Order
Routes are matched in this order:
- Static routes - Exact path matches
- Dynamic routes - Single parameter segments
- Optional routes - Optional parameter segments
- Catch-all routes - Rest parameters
/posts -> routes/posts/index.tsx (static)
/posts/hello-world -> routes/posts/[slug].tsx (dynamic)
/docs/a/b/c -> routes/docs/[...path].tsx (catch-all)Pattern Types
Static Segments
routes/about.tsx -> /about
routes/blog/index.tsx -> /blog
routes/api/health.tsx -> /api/healthDynamic Segments
Single parameter in brackets:
routes/users/[id].tsx -> /users/123
routes/posts/[slug].tsx -> /posts/hello-world
routes/[category]/[product].tsx -> /electronics/laptopAccess parameters in loader:
export const loader = createLoader(async ({ params }) => {
const { id } = params // { id: '123' }
return { user: await db.users.find(id) }
})Optional Segments
Use double brackets for optional parameters:
routes/posts/[[page]].tsx -> /posts or /posts/2export const loader = createLoader(async ({ params }) => {
const page = params.page ? parseInt(params.page) : 1
return { posts: await db.posts.paginate(page) }
})Catch-All Segments
Use [...name] for rest parameters:
routes/docs/[...path].tsx -> /docs/getting-started
-> /docs/api/core/create-app
-> /docs/a/b/c/dexport const loader = createLoader(async ({ params }) => {
const { path } = params // ['getting-started'] or ['api', 'core', 'create-app']
return { doc: await loadDoc(path) }
})Route Groups
Parentheses create route groups without affecting the URL:
routes/
(marketing)/
about.tsx -> /about
pricing.tsx -> /pricing
(app)/
dashboard.tsx -> /dashboard
settings.tsx -> /settingsGroups are useful for:
- Organizing related routes
- Applying different layouts to groups
Route Specificity
When multiple routes could match, the most specific wins:
/posts/new -> routes/posts/new.tsx (static wins)
/posts/hello -> routes/posts/[slug].tsx (dynamic)Specificity rules:
- More static segments = higher priority
- Dynamic segments beat catch-all
- Earlier position has more weight
Query Parameters
Query parameters are available via request.url:
export const loader = createLoader(async ({ request }) => {
const url = new URL(request.url)
const page = url.searchParams.get('page') || '1'
const sort = url.searchParams.get('sort') || 'date'
return { posts: await getPosts({ page, sort }) }
})