---
title: "Routing Utility"
description: "Routing utilities orchestrate incoming HTTP request evaluation, path matching, middleware coordination, and destination compilation across Next.js routing layers. Provided primarily via @next/routi..."
last_updated: "2026-10-12T06:38:56.853737+00:00"
canonical_url: "https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/companion-packages/routing-utility"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/next-routing/src/resolve-routes.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts)
- [packages/next/src/shared/lib/router/router.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/router.ts)
- [packages/next/src/shared/lib/router/utils/prepare-destination.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/utils/prepare-destination.ts)
- [packages/next/src/server/lib/router-utils/resolve-routes.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/router-utils/resolve-routes.ts)
- [packages/next/src/client/route-params.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/route-params.ts)
- [packages/next/src/server/base-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts)
- [packages/next-routing/src/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/index.ts)
- [packages/next/src/client/route-loader.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/route-loader.ts)
- [packages/next/src/client/resolve-href.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/resolve-href.ts)
- [packages/next/src/shared/lib/router/adapters.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/adapters.tsx)
- [packages/next/src/server/route-modules/route-module.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/route-modules/route-module.ts)
- [packages/next/src/client/components/navigation.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/components/navigation.ts)
- [packages/next/src/server/lib/router-utils/filesystem.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/router-utils/filesystem.ts)
- [packages/next/src/client/components/segment-cache/cache.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/components/segment-cache/cache.ts)
- [packages/next-routing/src/destination.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/destination.ts)
- [packages/next/src/build/webpack/loaders/next-route-loader/index.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/webpack/loaders/next-route-loader/index.ts)
- [packages/next/src/server/web/next-url.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/web/next-url.ts)
- [packages/next-routing/src/next-data.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/next-data.ts)
- [packages/next/src/server/server-utils.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/server-utils.ts)
- [packages/next/src/client/components/router-transition.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/components/router-transition.ts)
- [packages/next-routing/src/types.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/types.ts)
- [packages/eslint-plugin-next/src/utils/url.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/eslint-plugin-next/src/utils/url.ts)
- [packages/next/src/client/components/navigation-dynamic-rendering.browser.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/components/navigation-dynamic-rendering.browser.ts)
- [packages/next-routing/package.json](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/package.json)
</details>

## Overview

Routing utilities orchestrate incoming HTTP request evaluation, path matching, middleware coordination, and destination compilation across Next.js routing layers. Provided primarily via `@next/routing` and core router subsystems, these utilities resolve route definitions, handle internationalization rules, normalize data URLs, and synchronize parameter extraction between client and server runtime components.

Sources: [packages/next-routing/package.json:1-15](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/package.json#L1-L15), [packages/next-routing/src/resolve-routes.ts:557-601](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L557-L601)

## Public API Surface and Types

### Public Export Contract

The `@next/routing` package exposes route resolution functionality, middleware helpers, internationalization detectors, and core TypeScript definitions through its primary entry point. The public export contract includes the `resolveRoutes` function alongside type definitions for route matching conditions (`RouteHas`), route rules (`Route`), middleware context and results (`MiddlewareContext`, `MiddlewareResult`), and resolution parameters and outcomes (`ResolveRoutesParams`, `ResolveRoutesResult`, `ResolveRoutesQuery`, `RouteInvocationTarget`). Additionally, internationalization utilities (`detectLocale`, `detectDomainLocale`, `normalizeLocalePath`, `getAcceptLanguageLocale`, `getCookieLocale`, `I18nConfig`, `I18nDomain`) and the middleware response mapper (`responseToMiddlewareResult`) are exported for external consumers.

```typescript
export { resolveRoutes } from './resolve-routes'
export type {
  RouteHas,
  Route,
  MiddlewareContext,
  MiddlewareResult,
  ResolveRoutesParams,
  ResolveRoutesResult,
  ResolveRoutesQuery,
  ResolveRoutesQueryValue,
  RouteInvocationTarget,
} from './types'
export type { I18nConfig, I18nDomain } from './i18n'
export {
  detectLocale,
  detectDomainLocale,
  normalizeLocalePath,
  getAcceptLanguageLocale,
  getCookieLocale,
} from './i18n'
export { responseToMiddlewareResult } from './middleware'
```
Sources: [packages/next-routing/src/index.ts:1-21](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/index.ts#L1-L21)

### RouteHas Conditions and Route Configurations

Conditional route matching relies on `RouteHas` union types, which distinguish between request headers, cookies, query parameters, and hostnames. A standard matcher configuration evaluates `type` fields against `key` and optional `value` constraints, whereas host-based matchers omit the key and enforce a strict `value` string match.

```typescript
export type RouteHas =
  | {
      type: 'header' | 'cookie' | 'query'
      key: string
      value?: string
    }
  | {
      type: 'host'
      key?: undefined
      value: string
    }
```
Sources: [packages/next-routing/src/types.ts:1-11](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/types.ts#L1-L11)

Individual route entries (`Route`) define regex patterns, destination templates, response headers, status codes, and positive or negative match conditions (`has` and `missing`).

| Field | Type | Description |
| :--- | :--- | :--- |
| `sourceRegex` | `string` | Regular expression string with named or unnamed capture matches |
| `destination` | `string` (optional) | Replacement destination template using `$1` or `$name` tokens |
| `headers` | `Record<string, string>` (optional) | Custom headers to inject upon matching |
| `has` | `RouteHas[]` (optional) | Required conditions that must be satisfied |
| `missing` | `RouteHas[]` (optional) | Conditions that must not be present |
| `status` | `number` (optional) | HTTP status code associated with the route |

Sources: [packages/next-routing/src/types.ts:13-23](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/types.ts#L13-L23)

### Invocation Types and Resolution Parameters

Parameters for route resolution (`ResolveRoutesParams`) aggregate request properties, build identifiers, base paths, internationalization settings, and categorical route arrays (`beforeMiddleware`, `middlewareMatchers`, `beforeFiles`, `afterFiles`, `dynamicRoutes`, `onMatch`, and `fallback`). Route matching defaults to case-insensitivity to mirror default Next.js behavior unless `caseSensitive` is explicitly enabled.

```typescript
export type ResolveRoutesParams = {
  url: URL
  buildId: string
  basePath: string
  requestBody: ReadableStream
  headers: Headers
  pathnames: string[]
  i18n?: {
    defaultLocale: string
    domains?: Array<{
      defaultLocale: string
      domain: string
      http?: true
      locales?: string[]
    }>
    localeDetection?: false
    locales: string[]
  }
  routes: {
    /**
     * When false (default), route matching is case-insensitive to mirror
     * Next.js default behavior. When true, matches are case-sensitive.
     */
    caseSensitive?: boolean
    beforeMiddleware: Array<Route>
    /**
     * Middleware matcher definitions used to decide whether middleware should
     * be invoked for the current request.
     */
    middlewareMatchers?: Array<Route>
    beforeFiles: Array<Route>
    afterFiles: Array<Route>
    dynamicRoutes: Array<Route>
    onMatch: Array<Route>
    fallback: Array<Route>
    shouldNormalizeNextData?: boolean
  }
  invokeMiddleware: (ctx: MiddlewareContext) => Promise<MiddlewareResult>
}
```
Sources: [packages/next-routing/src/types.ts:42-80](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/types.ts#L42-L80)

The outcome of the resolution pipeline is structured via `ResolveRoutesResult`, which encapsulates middleware responses, external rewrites, redirects, resolved pathnames, merged query parameters, and a concrete `RouteInvocationTarget`.

```typescript
export type RouteInvocationTarget = {
  /**
   * Concrete pathname that should be invoked after routing resolution.
   */
  pathname: string
  /**
   * Concrete query that should be invoked after routing resolution.
   */
  query: ResolveRoutesQuery
}
```
Sources: [packages/next-routing/src/types.ts:85-94](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/types.ts#L85-L94)

> [!NOTE]
> For dynamic routes, `resolvedPathname` returns the matched template pathname rather than the instantiated concrete URL path.

Sources: [packages/next-routing/src/types.ts:103-107](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/types.ts#L103-L107)

## Route Matching and Evaluation Pipeline

### Request Evaluation Algorithm

Incoming requests flow through an ordered pipeline within `resolveRoutes` that handles URL initialization, internationalization prefixing, preprocessing via `beforeMiddleware`, middleware invocation, and localized redirects. Initial parameters such as headers, cookies, base paths, and build identifiers are unpacked, and `initialUrl` is copied into a mutable `currentUrl` instance for sequential transformations.

```typescript
export async function resolveRoutes(
  params: ResolveRoutesParams
): Promise<ResolveRoutesResult> {
  const {
    url: initialUrl,
    basePath,
    requestBody,
    headers: initialHeaders,
    pathnames,
    routes,
    invokeMiddleware,
    buildId,
    i18n,
  } = params

  const { shouldNormalizeNextData, caseSensitive = false } = routes

  let currentUrl = new URL(initialUrl.toString())
  let currentRequestHeaders = new Headers(initialHeaders)
  let currentResponseHeaders = new Headers()
  let currentStatus: number | undefined
  let pendingLocaleRedirect:
    | {
        url: URL
        status: number
      }
    | undefined
  let pendingBeforeMiddlewareStop:
    | {
        status: number | undefined
      }
    | undefined
  const initialOrigin = initialUrl.origin

  // Check if the original URL is a data URL and normalize if so
  let isDataUrl = false
  if (shouldNormalizeNextData) {
    const dataPrefix = `${basePath}/_next/data/${buildId}/`
    isDataUrl = initialUrl.pathname.startsWith(dataPrefix)

    if (isDataUrl) {
      currentUrl = normalizeNextDataUrl(currentUrl, basePath, buildId)
    }
  }

```
Sources: [packages/next-routing/src/resolve-routes.ts:557-601](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L557-L601)

If internationalization is configured and the route is not a Next.js data request or API path, the pipeline extracts locale information from the pathname or host headers. When locale auto-detection runs on index paths (`/`), a mismatched preferred locale triggers a pending redirect status of `307` to either a configured multi-domain target or a prefixed local path. Otherwise, the target locale is prefixed internally for subsequent route resolution phases without issuing an immediate client redirect.

```typescript
// Handle i18n locale detection and redirects
if (i18n && !isDataUrl) {
  const pathname = currentUrl.pathname.startsWith(basePath)
    ? currentUrl.pathname.slice(basePath.length) || '/'
    : currentUrl.pathname

  // Skip locale handling for _next and api routes
  if (!pathname.startsWith('/_next/') && !pathname.startsWith('/api/')) {
    const hostname = currentUrl.hostname
    const cookieHeader = currentRequestHeaders.get('cookie') || undefined
    const acceptLanguageHeader =
      currentRequestHeaders.get('accept-language') || undefined

    // Detect locale from path first
    const pathLocaleResult = normalizeLocalePath(pathname, i18n.locales)
    const localeInPath = !!pathLocaleResult.detectedLocale

    // Detect domain locale
    const domainLocale = detectDomainLocale(i18n.domains, hostname)
    const defaultLocale = domainLocale?.defaultLocale || i18n.defaultLocale

    // Determine target locale if locale detection is enabled
    let targetLocale = pathLocaleResult.detectedLocale || defaultLocale

    // Match Next.js behavior: preferred-locale auto-detection redirects only
    // on index requests, not on arbitrary non-locale pathnames.
    const shouldDetectPreferredLocale =
      i18n.localeDetection !== false &&
      !localeInPath &&
      pathLocaleResult.pathname === '/'

    if (shouldDetectPreferredLocale) {
      const detectedResult = detectLocale({
        pathname,
        hostname,
        cookieHeader,
        acceptLanguageHeader,
        i18n,
      })

      targetLocale = detectedResult.locale

      // Check if we need to redirect based on domain or locale mismatch
      if (targetLocale !== defaultLocale) {
        const targetDomain = detectDomainLocale(
          i18n.domains,
          undefined,
          targetLocale
        )

        // Redirect to different domain if target locale has a different configured domain
        if (targetDomain && targetDomain.domain !== hostname) {
          const scheme = targetDomain.http ? 'http' : 'https'
          const localePrefix =
            targetLocale === targetDomain.defaultLocale
              ? ''
              : `/${targetLocale}`
          const redirectUrl = new URL(
            `${scheme}://${targetDomain.domain}${basePath}${localePrefix}${pathname}${currentUrl.search}`
          )

          pendingLocaleRedirect = {
            url: redirectUrl,
            status: 307,
          }
        }

        // If no dedicated domain for target locale, or we're already on the right domain,
        // redirect to add locale prefix on same domain
        if (
          !targetDomain ||
          (targetDomain && targetDomain.domain === hostname)
        ) {
          const redirectUrl = new URL(currentUrl.toString())
          redirectUrl.pathname = `${basePath}/${targetLocale}${pathname}`

          pendingLocaleRedirect = {
            url: redirectUrl,
            status: 307,
          }
```
Sources: [packages/next-routing/src/resolve-routes.ts:602-693](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L602-L693)

> [!WARNING]
> Preferred-locale auto-detection only triggers redirect behavior on index requests where `pathpathname` equals `'/'`, and explicitly skips arbitrary non-locale pathnames to prevent infinite redirection loops on deep links.

Sources: [packages/next-routing/src/resolve-routes.ts:627-631](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L627-L631)

### Middleware Matching and Invocation Checks

Before invoking middleware functions, requests pass through the `beforeMiddleware` route processor. If a redirect or external rewrite occurs during this phase, execution halts immediately and returns the resolved headers and status.

```typescript
// Process beforeMiddleware routes
const beforeMiddlewareResult = processRoutes(
  routes.beforeMiddleware,
  currentUrl,
  currentRequestHeaders,
  currentResponseHeaders,
  initialOrigin,
  caseSensitive
)

if (beforeMiddlewareResult.status) {
  currentStatus = beforeMiddlewareResult.status
}

if (beforeMiddlewareResult.redirect) {
  return {
    redirect: beforeMiddlewareResult.redirect,
    resolvedHeaders: currentResponseHeaders,
    status: currentStatus,
  }
}

if (beforeMiddlewareResult.externalRewrite) {
  return {
    externalRewrite: beforeMiddlewareResult.externalRewrite,
    resolvedHeaders: currentResponseHeaders,
    status: currentStatus,
  }
}

if (beforeMiddlewareResult.stopped) {
  pendingBeforeMiddlewareStop = {
    status: currentStatus,
  }
}

currentUrl = beforeMiddlewareResult.url

```
Sources: [packages/next-routing/src/resolve-routes.ts:695-732](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L695-L732)

If data URL normalization was active, URLs are denormalized before evaluating middleware matchers via `shouldInvokeMiddlewareForRequest`. When middleware runs, it can mutate request headers, append response `set-cookie` headers, or return redirect and rewrite destinations.

```typescript
let middlewareInvocationUrl = currentUrl

// Denormalize before invoking middleware if this was originally a data URL
if (isDataUrl && shouldNormalizeNextData) {
  middlewareInvocationUrl = denormalizeNextDataUrl(
    currentUrl,
    basePath,
    buildId
  )
}

const shouldInvokeMiddleware = shouldInvokeMiddlewareForRequest(
  routes.middlewareMatchers,
  currentUrl,
  currentRequestHeaders,
  caseSensitive
)

if (shouldInvokeMiddleware) {
  // Invoke middleware
  const middlewareResult = await invokeMiddleware({
    url: middlewareInvocationUrl,
    headers: currentRequestHeaders,
    requestBody,
  })

  // Check if middleware sent the response body
  if (middlewareResult.bodySent) {
    return { middlewareResponded: true }
  }

  // Apply request headers from middleware
  if (middlewareResult.requestHeaders) {
    currentRequestHeaders = new Headers(middlewareResult.requestHeaders)
  }

  // Apply response headers from middleware
  if (middlewareResult.responseHeaders) {
    middlewareResult.responseHeaders.forEach((value, key) => {
      if (key.toLowerCase() === 'set-cookie') {
        currentResponseHeaders.append(key, value)
      } else {
        currentResponseHeaders.set(key, value)
      }
    })
  }

  // Handle middleware redirect
  if (middlewareResult.redirect) {
    if (!currentResponseHeaders.has('location')) {
      currentResponseHeaders.set(
        'Location',
        middlewareResult.redirect.url.toString()
      )
    }
    return {
      resolvedHeaders: currentResponseHeaders,
      status: middlewareResult.redirect.status,
    }
  }

  // Handle middleware rewrite
  if (middlewareResult.rewrite) {
    currentUrl = middlewareResult.rewrite

    // Check if it's an external rewrite
    if (currentUrl.origin !== initialOrigin) {
      return {
        externalRewrite: currentUrl,
        resolvedHeaders: currentResponseHeaders,
        status: currentStatus,
      }
    }
  }
}
```
Sources: [packages/next-routing/src/resolve-routes.ts:733-807](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L733-L807)

> [!NOTE]
> Response headers returned by middleware are appended normally for standard keys, but `set-cookie` headers utilize `.append()` to preserve multiple cookie definitions instead of overwriting existing entries.

Sources: [packages/next-routing/src/resolve-routes.ts:770-778](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L770-L778)

## Destination Replacement and URL Construction

### Dynamic Token Substitution and Placeholder Parsing

Destination string processing relies on token substitution mechanisms that map regular expression captures, named capture groups, and conditional `has` matches onto placeholder tokens. The `replaceDestination` utility extracts keys from `hasCaptures` and index-based or group-based regular expression matches, sorts them by length in descending order, and constructs an alternation regex pattern. Placeholder tokens matching digits are constrained with negative lookaheads to prevent partial numerical suffix collisions.

```typescript
export function replaceDestination(
  destination: string,
  regexMatches: RegExpMatchArray | null,
  hasCaptures: Record<string, string>
): string {
  const captureKeys = Object.keys(hasCaptures)

  if (regexMatches) {
    for (let index = 1; index < regexMatches.length; index++) {
      captureKeys.push(String(index))
    }
    if (regexMatches.groups) {
      captureKeys.push(...Object.keys(regexMatches.groups))
    }
  }

  if (captureKeys.length === 0) {
    return destination
  }

  const capturePattern = captureKeys
    .sort((first, second) => second.length - first.length)
    .map((key) => {
      const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
      return /^\d+$/.test(key) ? `${escapedKey}(?!\\d)` : escapedKey
    })
    .join('|')

  // Replace placeholders once. Captured paths can contain literal text such as
  // $2 or $d$segment that must not become another substitution.
  return destination.replace(
    new RegExp(`\\$(${capturePattern})`, 'g'),
    (placeholder, key: string) => {
      if (regexMatches) {
        const index = Number(key)
        if (
          Number.isInteger(index) &&
          index > 0 &&
          index < regexMatches.length &&
          String(index) === key
        ) {
          return regexMatches[index] ?? ''
        }
        if (regexMatches.groups && Object.hasOwn(regexMatches.groups, key)) {
          return regexMatches.groups[key] ?? ''
        }
      }
      if (Object.hasOwn(hasCaptures, key)) {
        return hasCaptures[key]
      }
      return placeholder
    }
  )
}
```
Sources: [packages/next-routing/src/destination.ts:5-58](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/destination.ts#L5-L58)

> [!WARNING]
> Placeholder replacement runs exactly once to prevent security vulnerabilities or replacement loops where captured paths containing literal strings like `$2` or `$d$segment` get re-evaluated as nested substitution tokens.

Sources: [packages/next-routing/src/destination.ts:33-35](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/destination.ts#L33-L35)

In more complex routing configurations, `prepareDestination` parses destination templates and parameter dictionaries through `path-to-regexp` compilers while disabling validation flags. Disabling compile validation prevents failures when merging named pattern parameters from a source route regex into an entirely separate destination template structure.

Sources: [packages/next/src/shared/lib/router/utils/prepare-destination.ts:254-263](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/utils/prepare-destination.ts#L254-L263)

### Query Merging and External Destination Handling

Once dynamic tokens are resolved, destination application handles URL mutation or absolute redirection based on protocol inspection. The `isExternalDestination` function checks whether a destination string begins with explicit protocol prefixes.

```typescript
export function isExternalDestination(destination: string): boolean {
  return destination.startsWith('http://') || destination.startsWith('https://')
}
```
Sources: [packages/next-routing/src/destination.ts:63-65](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/destination.ts#L63-L65)

When applying internal or external destinations via `applyDestination`, the utility splits the destination string into pathname and search components. Query parameters parsed from the destination search string are merged into the target URL parameters using `URLSearchParams` iteration, updating existing keys or appending new values accordingly.

```typescript
export function applyDestination(currentUrl: URL, destination: string): URL {
  if (isExternalDestination(destination)) {
    return new URL(destination)
  }

  // Create a new URL with the updated pathname
  const newUrl = new URL(currentUrl.toString())

  // Handle destinations with query strings
  const [pathname, search] = destination.split('?')
  newUrl.pathname = pathname

  if (search) {
    // Merge query parameters
    const newParams = new URLSearchParams(search)
    for (const [key, value] of newParams.entries()) {
      newUrl.searchParams.set(key, value)
    }
  }

  return newUrl
}
```
Sources: [packages/next-routing/src/destination.ts:71-92](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/destination.ts#L71-L92)

> [!NOTE]
> If a destination is identified as an external rewrite or redirect target via `isExternalDestination`, `applyDestination` bypasses query parameter merging on the current URL base and immediately constructs and returns a brand-new `URL` instance pointing to the external destination.

Sources: [packages/next-routing/src/destination.ts:72-74](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/destination.ts#L72-L74)

## Next Data URL Normalization Cycle

### Data URL Detection and Path Normalization

When Next.js routing handles requests, standalone data route resolution depends on normalizing incoming client requests that point to data route bundles. The routing lifecycle inspects whether data route normalization is enabled via `shouldNormalizeNextData`, identifying requests starting with the data prefix `${basePath}/_next/data/${buildId}/`. 

```typescript
let isDataUrl = false
if (shouldNormalizeNextData) {
  const dataPrefix = `${basePath}/_next/data/${buildId}/`
  isDataUrl = initialUrl.pathname.startsWith(dataPrefix)

  if (isDataUrl) {
    currentUrl = normalizeNextDataUrl(currentUrl, basePath, buildId)
  }
}
```
Sources: [packages/next-routing/src/resolve-routes.ts:592-600](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L592-L600)

When a request matches this structure, `normalizeNextDataUrl` strips the `/_next/data/${buildId}/` segment and removes any trailing `.json` file extension from the path. If the resulting path equals `index`, it maps back to the root `basePath` or `/`. Otherwise, it reconstructs the standard pathname combining the base path and the remaining route segments.

```typescript
export function normalizeNextDataUrl(
  url: URL,
  basePath: string,
  buildId: string
): URL {
  const newUrl = new URL(url.toString())
  let pathname = newUrl.pathname

  // Pattern: ${basePath}/_next/data/${buildId}/${path}.json
  const dataPrefix = `${basePath}/_next/data/${buildId}/`

  if (pathname.startsWith(dataPrefix)) {
    // Remove the /_next/data/${buildId}/ part, keeping what comes after
    let pathAfterData = pathname.slice(dataPrefix.length)

    // Remove .json extension if present
    if (pathAfterData.endsWith('.json')) {
      pathAfterData = pathAfterData.slice(0, -5)
    }

    if (pathAfterData === 'index') {
      pathname = basePath || '/'
    } else {
      pathname = basePath ? `${basePath}/${pathAfterData}` : `/${pathAfterData}`
    }
    newUrl.pathname = pathname
  }

  return newUrl
}
```
Sources: [packages/next-routing/src/next-data.ts:5-34](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/next-data.ts#L5-L34)

> [!NOTE]
> During normalization, an empty or root-level index data request like `/_next/data/build-id/index.json` is explicitly transformed to match the root `basePath` rather than an empty string pathname.

Sources: [packages/next-routing/src/next-data.ts:25-27](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/next-data.ts#L25-L27)

### Denormalization and Re-attachment Cycle

Before middleware invocation, the routing cycle denormalizes the normalized URL back into its data route format so that middleware matchers and rewrite rules inspect the original data path structure. 

```typescript
// Denormalize before invoking middleware if this was originally a data URL
if (isDataUrl && shouldNormalizeNextData) {
  middlewareInvocationUrl = denormalizeNextDataUrl(
    currentUrl,
    basePath,
    buildId
  )
}
```
Sources: [packages/next-routing/src/resolve-routes.ts:735-742](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L735-L742)

The `denormalizeNextDataUrl` function checks if the current URL lacks the data prefix. If absent, it strips the `basePath`, maps empty or root paths to `/index`, and re-attaches the `/_next/data/${buildId}/` prefix alongside the `.json` extension.

```typescript
export function denormalizeNextDataUrl(
  url: URL,
  basePath: string,
  buildId: string
): URL {
  const newUrl = new URL(url.toString())
  let pathname = newUrl.pathname

  // Only denormalize if it's not already a data URL
  const dataPrefix = `${basePath}/_next/data/${buildId}/`
  if (!pathname.startsWith(dataPrefix)) {
    // Remove basePath if present
    let pathWithoutBase = pathname
    if (basePath && pathname.startsWith(basePath)) {
      pathWithoutBase = pathname.slice(basePath.length)
    }

    if (pathWithoutBase === '' || pathWithoutBase === '/') {
      pathWithoutBase = '/index'
    }

    // Add the /_next/data/${buildId}/ prefix and .json extension
    pathname = `${basePath}/_next/data/${buildId}${pathWithoutBase}.json`
    newUrl.pathname = pathname
  }

  return newUrl
```
Sources: [packages/next-routing/src/next-data.ts:40-66](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/next-data.ts#L40-L66)

> [!WARNING]
> Denormalization execution is conditional: `denormalizeNextDataUrl` checks whether `!pathname.startsWith(dataPrefix)` to prevent recursive prefix duplication if a URL has already been transformed.

Sources: [packages/next-routing/src/next-data.ts:49-50](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/next-data.ts#L49-L50)

Following middleware execution, if the request was originally flagged as a data URL, the routing engine normalizes the path a second time prior to evaluating `beforeFiles` route matching rules.

```typescript
// Normalize again after middleware if this was originally a data URL
if (isDataUrl && shouldNormalizeNextData) {
  currentUrl = normalizeNextDataUrl(currentUrl, basePath, buildId)
}
```
Sources: [packages/next-routing/src/resolve-routes.ts:829-832](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-routing/src/resolve-routes.ts#L829-L832)

## Server and Client Routing Integration

### Server Route Resolution and Filesystem Synchronization

Server-side routing operations coordinate request paths with concrete filesystem outputs using filesystem checkers and custom route tables. When `resolveRoutes` executes, it evaluates route execution flows sequentially against custom rewrites, redirects, `before_files`, `middleware`, and static filesystem assets via `fsChecker.getItem(pathname)`.

```typescript
const output = await fsChecker.getItem(pathname)
```
Sources: [packages/next/src/server/lib/router-utils/resolve-routes.ts:504-504](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/router-utils/resolve-routes.ts#L504-L504)

> [!NOTE]
> Only page and app file outputs participate in route rendering. Public, static, image, and virtual filesystem items bypass template rendering and are served directly as static filesystem assets.

Sources: [packages/next/src/server/lib/router-utils/filesystem.ts:845-849](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/lib/router-utils/filesystem.ts#L845-L849)

Client-side navigation state bridges runtime parameter parsing through helper utilities like `adaptForPathParams` and `adaptForSearchParams`. These adapters translate the current browser router state into structured `Params` and `URLSearchParams` instances matching server expectations.

```tsx
export function adaptForPathParams(
  router: Pick<NextRouter, 'isReady' | 'pathname' | 'query' | 'asPath'>
): Params | null {
  if (!router.isReady || !router.query) {
    return null
  }
  const pathParams: Params = {}
  const routeRegex = getRouteRegex(router.pathname)
  const keys = Object.keys(routeRegex.groups)
  for (const key of keys) {
    pathParams[key] = router.query[key]!
  }
  return pathParams
}
```
Sources: [packages/next/src/shared/lib/router/adapters.tsx:57-70](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/adapters.tsx#L57-L70)

### URL Normalization and Canonical Parameter Decoding

Client-side href resolution handles parameter interpolation via `resolveHref`, normalizing paths and testing local URL validity before routing. 

```typescript
export function resolveHref(
  router: NextRouter,
  href: Url,
  resolveAs: true
): [string, string] | [string]
export function resolveHref(
  router: NextRouter,
  href: Url,
  resolveAs?: false
): string
export function resolveHref(
  router: NextRouter,
  href: Url,
  resolveAs?: boolean
): [string, string] | [string] | string {
```
Sources: [packages/next/src/client/resolve-href.ts:18-32](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/resolve-href.ts#L18-L32)

To maintain consistent encodings between client URL parts and server-side route definitions, `canonicalizeURLPart` decodes incoming path segments and re-encodes them using `encodeURIComponent`. This prevents double-encoding issues for percent-encoded sequences like `%2F`.

```typescript
export function canonicalizeURLPart(part: string): string {
  try {
    return encodeURIComponent(decodeURIComponent(part))
  } catch {
    // `decodeURIComponent` throws on malformed sequences. Fall back to the
    // already-encoded form rather than failing the navigation.
    return part
  }
}
```
Sources: [packages/next/src/client/route-params.ts:86-94](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/route-params.ts#L86-L94)

> [!WARNING]
> The browser URL parser and server-side dynamic param decoders handle special characters differently; URL parsers leave characters like `,` and `:` untouched, whereas `encodeURIComponent` percent-encodes them.

Sources: [packages/next/src/client/route-params.ts:78-85](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/route-params.ts#L78-L85)

### Path and Search Param Synchronization

| Adapter / Function | Target Type | Input Source | Synchronization Behavior |
| :--- | :--- | :--- | :--- |
| `adaptForSearchParams` | `URLSearchParams` | `router.asPath` | Extracts query parameters from the active browser path string. |
| `adaptForPathParams` | `Params` | `router.query` | Extracts dynamic route parameters keyed by route regex group names. |
| `canonicalizeURLPart` | `string` | URL pathname part | Decodes and re-encodes path segments to match server-side parameters. |
| `getRenderedPathname` | `NormalizedPathname` | `RSCResponse` | Resolves pathnames using server headers or response URLs. |

Sources: [packages/next/src/shared/lib/router/adapters.tsx:47-70](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/adapters.tsx#L47-L70), [packages/next/src/client/route-params.ts:48-63](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/route-params.ts#L48-L63), [packages/next/src/client/route-params.ts:86-94](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/route-params.ts#L86-L94)

## Related

- [Request Lifecycle](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/request-lifecycle)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/llms.txt) for all pages in this wiki.
