Skip to main content
The Next.js Vercel Adapter generates sophisticated routing configurations that handle rewrites, redirects, headers, middleware, and various Next.js features. This page explains the routing system and customization options.

Route generation order

Routes are generated in a specific order (defined in index.ts:261-936) to ensure correct precedence:
vercelConfig.routes = [
  // 1. Priority redirects
  // 2. Next.js data URL normalization
  // 3. i18n locale handling
  // 4. User headers
  // 5. User redirects
  // 6. Middleware routes
  // 7. beforeFiles rewrites
  // 8. filesystem check
  // 9. afterFiles rewrites
  // 10. resource check
  // 11. fallback rewrites
  // 12. Dynamic routes
  // 13. Error handling
]
Route order is critical. Changing the order can break authentication, redirects, or cause incorrect page rendering. The adapter follows Next.js’s routing semantics exactly.

Rewrite handling

The adapter extracts and normalizes rewrites from three phases:

Rewrite phases

Run before filesystem checks, allowing you to rewrite before Next.js looks for static files:
// routing.ts:161-167
beforeFiles: routing.beforeFiles.filter(isRewriteRoute).map((item) => {
  const route = normalize(item);
  delete route.check;      // No filesystem check
  route.continue = true;   // Continue to next routes
  route.override = true;   // Can override later routes
  return route;
})
These rewrites set special headers to track the rewritten path.
Run after filesystem check but before dynamic routes:
// routing.ts:168-169
afterFiles: routing.afterFiles.filter(isRewriteRoute).map(normalize)
These include check: true to verify the destination exists.
Run after all other routing logic as a final catch-all:
// routing.ts:169-170
fallback: routing.fallback.filter(isRewriteRoute).map(normalize)

Rewrite headers

The adapter adds tracking headers to rewrites for App Router:
// routing.ts:108-123
rewrite.headers = {
  ...rewrite.headers,
  'x-nextjs-rewritten-path': pathname,
  'x-nextjs-rewritten-query': query
}
These headers allow Next.js to properly handle RSC requests and prefetching after rewrites.

RSC and prefetch routing

App Router applications require special routes for React Server Components:

Standard RSC routes

// Rewrite requests with RSC header to .rsc files
{
  "src": "^/(?!.+\\.rsc)(.+?)(?:/)?$",
  "dest": "/$1.rsc",
  "has": [{
    "type": "header",
    "key": "Next-Router-State-Tree",
    "value": "1"
  }],
  "headers": {
    "vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
  },
  "continue": true,
  "override": true
}

Segment prefetch routes

For PPR (Partial Prerendering), the adapter generates segment prefetch routes:
// index.ts:500-559
{
  src: '/(?<path>.+?)(?:/)?$',
  dest: '/$path.segments/$segmentPath.segment.rsc',
  has: [
    { type: 'header', key: 'Next-Router-State-Tree', value: '1' },
    { type: 'header', key: 'Next-Router-Prefetch', value: '1' },
    { type: 'header', key: 'Next-Router-Segment-Prefetch', value: '/(?<segmentPath>.+)' }
  ]
}
These routes enable granular prefetching of page segments.

Middleware routing

Middleware generates routes with special configuration:
// outputs.ts:812-827
for (const matcher of output.config.matchers || []) {
  const route: RouteWithSrc = {
    continue: true,
    has: matcher.has,
    src: matcher.sourceRegex,
    missing: matcher.missing,
    middlewarePath: output.pathname,
    middlewareRawSrc: [matcher.source],
    override: true
  };
  routes.push(route);
}
The middlewarePath and middlewareRawSrc fields tell Vercel which function to invoke for middleware processing. The override: true flag ensures middleware runs before other routes.

Redirect patterns

The adapter extracts redirects from routing configuration:

Priority vs normal redirects

// routing.ts:176-211
function extractRedirects(routing) {
  const priorityRedirects: RouteWithSrc[] = [];
  const normalRedirects: RouteWithSrc[] = [];
  
  for (const route of routes) {
    if (!isRedirectStatus(route.status)) continue;
    
    if (route.priority) {
      vercelRoute.continue = true;  // Allow fallthrough
      priorityRedirects.push(vercelRoute);
    } else {
      normalRedirects.push(vercelRoute);
    }
  }
}
Priority redirects run first and can be overridden by later routes. Normal redirects are final.

Redirect status codes

The adapter recognizes these redirect statuses:
  • 301: Permanent redirect (cached by browsers)
  • 302: Temporary redirect
  • 303: See Other (POST → GET)
  • 307: Temporary redirect (preserves method)
  • 308: Permanent redirect (preserves method)

Header routes

Custom headers are extracted and applied:
// routing.ts:216-244
export function extractHeaders(routing) {
  const headers: RouteWithSrc[] = [];
  
  for (const route of routes) {
    // Headers have headers but are not redirects
    if (!route.headers || isRedirectStatus(route.status)) continue;
    // Skip if this is a rewrite (has destination)
    if (route.destination) continue;
    
    headers.push({
      src: route.sourceRegex,
      headers: route.headers,
      continue: true,
      has: route.has,
      missing: route.missing,
      ...(route.priority ? { important: true } : {})
    });
  }
}
Headers with important: true cannot be overridden by later routes.

i18n routing

For internationalized applications, the adapter generates complex locale handling:

Locale detection

{
  "src": "/",
  "locale": {
    "redirect": {
      "en": "/",
      "fr": "/fr",
      "de": "/de"
    },
    "cookie": "NEXT_LOCALE"
  },
  "continue": true
}
This route (generated in index.ts:360-384) redirects users based on:
  1. Accept-Language header
  2. NEXT_LOCALE cookie
  3. Default locale as fallback

Domain-based routing

For domain-specific locales:
// index.ts:322-357
{
  src: '^/(en|fr|de)?/?$',
  locale: {
    redirect: {
      en: 'https://example.com/',
      fr: 'https://example.fr/',
      de: 'https://example.de/'
    },
    cookie: 'NEXT_LOCALE'
  }
}

Locale prefix handling

// Auto-prefix non-locale paths with default locale
{
  src: '^/(?!(?:_next/.*|en|fr|de)(?:/.*|$))(.*)$',
  dest: '/$defaultLocale/$1',
  continue: true
}
This rewrites /about/en/about for the default locale.

Next.js data routes

The adapter handles _next/data routes for Pages Router:

Data URL normalization

// routing.ts:246-318
export function normalizeNextDataRoutes(
  config: NextConfig,
  buildId: string,
  shouldHandleMiddlewareDataResolving: boolean
): RouteWithSrc[] {
  return [
    // Add x-nextjs-data header
    {
      src: '/_next/data/(.*)',
      missing: [{ type: 'header', key: 'x-nextjs-data' }],
      transforms: [{
        type: 'request.headers',
        op: 'append',
        target: { key: 'x-nextjs-data' },
        args: '1'
      }]
    },
    // Strip _next/data prefix: /data/BUILD_ID/page.json → /page
    {
      src: `^/_next/data/${buildId}/(.*).json`,
      dest: '/$1',
      continue: true,
      has: [{ type: 'header', key: 'x-nextjs-data' }]
    }
  ];
}

Data denormalization

After routing, data URLs are converted back:
// routing.ts:337-392
export function denormalizeNextDataRoutes(
  config: NextConfig,
  buildId: string,
  shouldHandleMiddlewareDataResolving: boolean
): RouteWithSrc[] {
  return [
    // / → /_next/data/BUILD_ID/index.json
    { src: '^/$', dest: `/_next/data/${buildId}/index.json` },
    // /page → /_next/data/BUILD_ID/page.json
    { src: '^/((?!_next/)(?:.*[^/]|.*))/?$', dest: `/_next/data/${buildId}/$1.json` }
  ];
}
This ensures middleware receives normalized URLs but functions receive proper data URLs.

Dynamic route matching

Dynamic routes are converted from Next.js format to Vercel format:
// index.ts:231-259
for (const route of routing.dynamicRoutes) {
  dynamicRoutes.push({
    src: route.sourceRegex,     // e.g., '^/posts/(?<slug>[^/]+?)(?:/)?$'
    dest: route.destination,     // e.g., '/posts/[slug]'
    check: true,                 // Verify function exists
    has: route.has,              // Conditional matching
    missing: route.missing       // Negative conditions
  });
}

Custom routing patterns

Conditional routing with has and missing

You can use header/cookie/query conditions:
// Route only matches if conditions are met
{
  src: '/api/.*',
  dest: '/api/v2/$1',
  has: [
    { type: 'header', key: 'x-api-version', value: '2' }
  ],
  missing: [
    { type: 'cookie', key: 'legacy' }
  ]
}

onMatch routes

Special routes that run during the “hit” phase:
// routing.ts:320-335
export function extractOnMatchRoutes(routing: { onMatch: AdapterRoute[] }) {
  return routing.onMatch.map((route) => ({
    src: route.sourceRegex,
    dest: route.destination,
    headers: route.headers,
    continue: true,
    important: true  // Cannot be overridden
  }));
}
These routes run after the matched path is determined but before the function executes.

Routing handles

The adapter uses special “handles” to control routing flow:
  • { handle: 'filesystem' } - Check if static file exists
  • { handle: 'resource' } - Check for built assets
  • { handle: 'miss' } - No match found, continue to fallback
  • { handle: 'rewrite' } - Apply rewrite rules
  • { handle: 'hit' } - Match found, prepare for execution
  • { handle: 'error' } - Error occurred, show error page
These handles provide insertion points for different routing phases.

Debugging routing

To debug routing configuration:
# View all routes in order
cat .next/output/config.json | jq '.routes'

# Find routes matching a pattern
cat .next/output/config.json | jq '.routes[] | select(.src | contains("api"))'

# Check middleware routes
cat .next/output/config.json | jq '.routes[] | select(.middlewarePath)'

# View rewrite headers
cat .next/output/config.json | jq '.routes[] | select(.headers["x-nextjs-rewritten-path"])'
Understanding the routing configuration helps diagnose issues with redirects, rewrites, and middleware. The generated routes exactly mirror Next.js’s routing behavior.

Build docs developers (and LLMs) love