Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
Route matching in Next.js is the foundational mechanism responsible for mapping incoming HTTP requests or client-side navigation paths to the correct App Router (app/) or Pages Router (pages/) execution handler. When a request hits the server or a client initiates a transition, Next.js must resolve file-system routes, dynamic slugs, internationalized locales, and API endpoints without ambiguity. The subsystem solves this through an extensible architecture composed of route matcher providers, individual route matchers, route pattern normalizers, and the DefaultRouteMatcherManager.
The design separates manifest loading and route pattern transformation from the active lookup engine. Matchers are divided into static and dynamic collections, with dynamic routes sorted deterministically using specificity rules. Furthermore, client-side optimistic route prediction uses a trie-based structure to bypass server round-trips for known paths. This wiki page details the architecture, request pipelines, execution walkthroughs, and trade-offs of the Next.js route matching subsystem.
The route matching subsystem relies on route matcher providers to ingest build-time manifests (such as pages-manifest.json and app-paths-manifest.json) and transform them into concrete RouteMatcher instances. The base server configures a DefaultRouteMatcherManager by pushing provider instances through BaseServer.getRouteMatchers().
The supported provider classes and their corresponding manifests and route kinds include:
Sources: packages/next/src/server/route-matcher-providers/pages-route-matcher-provider.ts:16-82, packages/next/src/server/route-matcher-providers/app-page-route-matcher-provider.ts:13-60, packages/next/src/server/route-matcher-providers/app-route-route-matcher-provider.ts:12-47
At the core of individual route matching is the RouteMatcher class, which wraps a RouteDefinition and optionally compiles a dynamic regular expression matcher if the pathname contains dynamic route segments. Specialized subclasses like LocaleRouteMatcher, PagesRouteMatcher, and AppPageRouteMatcher extend this behavior to handle internationalization or specific route metadata.
Sources: packages/next/src/server/route-matchers/route-matcher.ts:16-66, packages/next/src/server/route-matchers/locale-route-matcher.ts:15-85
When RouteMatcher.test(pathname) is invoked, it checks whether the matcher is dynamic. If this.dynamic is defined, it executes the compiled RouteMatchFn to extract route parameters; otherwise, it performs an exact string comparison:
public test(pathname: string): RouteMatchResult | null {
if (this.dynamic) {
const params = this.dynamic(pathname)
if (!params) return null
return { params }
}
if (pathname === this.definition.pathname) {
return {}
}
return null
}The DefaultRouteMatcherManager orchestrates multiple providers and manages collections of static and dynamic matchers. During initialization or reload, it sorts dynamic routes using getSortedRoutes to ensure that specific static segments take precedence over dynamic slugs and catch-all routes.
Warning
If a match is attempted before route compilation finishes (this.lastCompilationID !== this.compilationID), the manager throws an invariant error: 'Invariant: expected routes to have been loaded before match'. This guards against requests racing route initialization.
When Next.js processes an incoming request inside NextServer.handleRequest (or the router server), route matching follows a precise, multi-step pipeline starting from URL normalization and locale analysis down to matcher evaluation and meta attachment.
The call chain proceeds as follows:
BaseServer / AppPageRouteModule.normalizeUrl): The incoming URL pathname has repeated slashes normalized, trailing slashes adjusted, and RSC or prefetch headers inspected (RSCPathnameNormalizer, SegmentPrefixRSCPathnameNormalizer).I18NProvider): If internationalization is configured, the request pathname is analyzed to detect and strip domain or path locales.DefaultRouteMatcherManager.match): The normalized pathname (with a guaranteed leading slash via ensureLeadingSlash) is passed to matchers.match(pathname, options).Sources: packages/next/src/server/route-matcher-managers/default-route-matcher-manager.ts:245-263, packages/next/src/server/route-modules/app-page/module.ts:116-153
validate): matchAll first checks this.matchers.static. If !isDynamicRoute(pathname), static matchers are tested instantly against the pathname.this.matchers.dynamic are evaluated in sorted order of specificity.RouteMatch object containing the definition and params is returned, it is attached to request metadata via addRequestMeta(req, 'match', match) to prevent redundant matching during subsequent rendering phases.Sources: packages/next/src/server/next-server.ts:1117-1120, packages/next/src/server/route-matcher-managers/default-route-matcher-manager.ts:264-287
In the App Router client, the segment cache implements optimistic routing via optimistic-routes.ts. This module predicts route structures for URLs that have not been prefetched yet by traversing a trie of KnownRoutePart nodes.
When matchKnownRoute(now, pathname, search) is called, it splits the pathname into segments and invokes matchKnownRoutePart:
/blog/featured from incorrectly matching /blog/[slug].[param], type 'd'), required catch-alls ([...param], type 'c'), and optional catch-alls (...param, type 'oc'), accumulating parameter values in resolvedParams.hasDynamicRewrite), the pattern flags the entry and bails out to server resolution.Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:607-693, packages/next/src/client/components/segment-cache/optimistic-routes.ts:743-845
Sources: packages/next/src/server/route-matcher-managers/default-route-matcher-manager.ts:13-17, packages/next/src/client/components/segment-cache/optimistic-routes.ts:33-44
The following example demonstrates how BaseServer initializes route matchers and how NextServer consumes route matches to execute Pages API routes or render pages:
import { DefaultRouteMatcherManager } from './route-matcher-managers/default-route-matcher-manager'
import { PagesRouteMatcherProvider } from './route-matcher-providers/pages-route-matcher-provider'
import { ServerManifestLoader } from './route-matcher-providers/helpers/manifest-loaders/server-manifest-loader'
import { PAGES_MANIFEST } from '../shared/lib/constants'
// 1. Create a server manifest loader
const manifestLoader = new ServerManifestLoader((name) => {
if (name === PAGES_MANIFEST) {
return { '/about': 'pages/about.js' }
}
return null
})
// 2. Instantiate matcher manager and register provider
const matchers = new DefaultRouteMatcherManager()
matchers.push(new PagesRouteMatcherProvider('/dist', manifestLoader))
// 3. Wait for readiness and match a pathname
await matchers.waitTillReady()
const match = await matchers.match('/about', {})
if (match) {
console.log(`Matched route definition:`, match.definition.page)
// Output: Matched route definition: /about
}Sources: packages/next/src/server/base-server.ts:802-848, packages/next/src/server/route-matcher-managers/default-route-matcher-manager.ts:37-53