Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Route Handlers

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

Route Handlers provide a unified mechanism for executing custom server-side request handlers and API endpoints within Next.js, bridging both the App Router (via AppRouteRouteModule) and the Pages API router (via PagesAPIRouteModule). By building upon the foundational RouteModule class, route handlers establish a consistent architecture for environment initialization, manifest loading, error reporting, and request preparation across diverse runtimes.

Sources: packages/next/src/server/route-modules/app-route/module.ts:10-15, packages/next/src/server/route-modules/pages-api/module.ts:8-10, packages/next/src/server/route-modules/route-module.ts:119-164

Route Module Base Architecture

Execution Walkthrough and Sequence

The abstract RouteModule class serves as the core foundation for all Next.js route handlers, providing a shared environment initialization, manifest loading, error reporting, and request preparation pipeline. During initialization, the constructor accepts RouteModuleOptions containing the definition, userland module, distDir, and relativeProjectDir, setting up the canonical cacheOwner via getResponseCacheOwner(definition) and detecting the development runtime mode through process.env.__NEXT_DEV_SERVER.

Sources: packages/next/src/server/route-modules/route-module.ts:89-164

When a request arrives at a route, the execution flows through the entry point template handler and initializes through the base class. The invocation sequence executes in the following call order: handler → prepare → prepareImpl → patchSetHeaderWithCookieSupport → getRequestMeta.

  1. handler: The template entry function receives the incoming request and response objects, attaches request metadata if provided, and invokes routeModule.prepare(). Sources: packages/next/src/build/templates/app-route.ts:106-137

  2. prepare: Traces the preparation span using the tracer utility and delegates the execution to prepareImpl(). Sources: packages/next/src/server/route-modules/route-module.ts:635-648

  3. prepareImpl: Evaluates runtime environment conditions, patches headers, resolves absolute project directories, loads route manifests, normalizes URLs, handles localization, and processes rewrites and dynamic route parameters. Sources: packages/next/src/server/route-modules/route-module.ts:650-735

  4. patchSetHeaderWithCookieSupport: Wraps the response setHeader method to intercept set-cookie headers, ensuring middleware cookies are properly merged rather than overridden by downstream handlers. Sources: packages/next/src/server/lib/patch-set-header.ts:18-65, packages/next/src/server/route-modules/route-module.ts:707-710

  5. getRequestMeta: Retrieves internal request metadata properties such as middlewareCookie, relativeProjectDir, and distDir from the request symbol storage. Sources: packages/next/src/server/request-meta.ts:385-399, packages/next/src/server/lib/patch-set-header.ts:43-43

Sources: packages/next/src/build/templates/app-route.ts:134-137, packages/next/src/server/route-modules/route-module.ts:635-648, packages/next/src/server/route-modules/route-module.ts:707-710, packages/next/src/server/lib/patch-set-header.ts:18-43, packages/next/src/server/request-meta.ts:385-399

Note

During header patching, patchSetHeaderWithCookieSupport intercepts calls to res.setHeader. When the header name matches set-cookie, it pulls any pending cookies set by middleware via getRequestMeta(req, 'middlewareCookie') and combines them with unique entries to prevent downstream overwrites.

Sources: packages/next/src/server/lib/patch-set-header.ts:32-62

App Route Module Execution

Dynamic Dispatch and HTTP Method Resolution

Incoming requests to an app route undergo preparation via prepareExecution, which ensures the userland module is fully loaded through this.ensureUserland(). In Turbopack development mode, the module resolver fetches the live userland module on each request via this._getUserland?.() to reflect immediate HMR updates, falling back to cached modules if top-level await yields a pending promise. The target HTTP method is inspected against the loaded userland module using this.resolveHandlerFromUserland(req.method, liveUserland) or this.resolveHandler(req.method).

Sources: packages/next/src/server/route-modules/app-route/module.ts:851-874

Sources: packages/next/src/server/route-modules/app-route/module.ts:851-913

Execution Lifecycle and Dynamic Behavior Modes

Once prepared, executeRequest runs the handler within asynchronous storage scopes (actionAsyncStorage, workUnitAsyncStorage, and workAsyncStorage). The request object is wrapped or proxied depending on the route's dynamic rendering configuration.

Dynamic ModeStore Property MutatedRequest Modification
force-dynamicworkStore.forceDynamic = trueNone (Standard NextRequest)
force-staticworkStore.forceStatic = truenew Proxy(req, forceStaticRequestHandlers)
errorworkStore.dynamicShouldError = trueNone (Standard NextRequest)
auto / undefinedNoneproxyNextRequest(req, workStore)

Sources: packages/next/src/server/route-modules/app-route/module.ts:939-972

Warning

If a route handler returns an object that is not an instance of the global Response class, validateExecutionResponse catches it and immediately returns a fallback HTTP 500 internal error response. Furthermore, headers containing x-middleware-rewrite or x-middleware-next set to '1' throw explicit runtime errors because middleware-specific response directives are unsupported inside app route handlers.

Sources: packages/next/src/server/route-modules/app-route/module.ts:915-937

Node App Route Invocation

Request Normalization and Manifest Caching

When an app route is invoked via the Node runtime template, the request enters the exported handler function where request metadata is attached and page paths are normalized. Specifically, Turbopack and Webpack route paths ending with /index or mapped via multi-zone draft modes are adjusted before invoking routeModule.prepare(req, res, { srcPage, multiZoneDraftMode }). This preparation method executes inside a tracing span using RouteModuleSpan.prepare, wrapping an internal call to prepareImpl which handles directory resolution, cookie patch support, and instrumentation registration.

Sources: packages/next/src/build/templates/app-route.ts:106-137, packages/next/src/server/route-modules/route-module.ts:635-648, packages/next/src/server/route-modules/route-module.ts:707-735

Sources: packages/next/src/build/templates/app-route.ts:134-161, packages/next/src/server/route-modules/route-module.ts:254-256, packages/next/src/server/route-modules/route-module.ts:635-648, packages/next/src/server/route-modules/route-module.ts:736-741

Within prepareImpl, manifest files are loaded via loadManifests under the RouteModuleSpan.loadManifests tracer span. Depending on whether the runtime is set to edge or node, manifests are either parsed from global variables using maybeJSONParse or read from disk relative to the project directory.

Note

In the edge runtime, preview properties are retrieved via getEdgePreviewProps(), which extracts configuration directly from process.env.__NEXT_PREVIEW_MODE_ID, process.env.__NEXT_PREVIEW_MODE_SIGNING_KEY, and process.env.__NEXT_PREVIEW_MODE_ENCRYPTION_KEY.

Sources: packages/next/src/server/route-modules/route-module.ts:254-274, packages/next/src/server/route-modules/route-module.ts:736-741, packages/next/src/server/web/get-edge-preview-props.ts:6-12

Response Wrapping and Execution Flow

Once preparation yields a valid configuration, handler initializes the incremental cache and sets up global singletons for server actions and client reference manifests if present. Standard Node IncomingMessage and ServerResponse instances are adapted into NodeNextRequest, NodeNextResponse, and standard Web NextRequest objects via NextRequestAdapter.fromNodeNextRequest.

Sources: packages/next/src/build/templates/app-route.ts:139-278

Manifest CategoryNode Runtime Source / MethodEdge Runtime Source / Fallback
Routes ManifestloadManifestFromRelativePath(ROUTES_MANIFEST)process.env.__NEXT_REWRITES / defaults
Prerender ManifestloadManifestFromRelativePath(PRERENDER_MANIFEST)Static empty routes object
Client ReferenceevalManifestFromRelativePath(...)self.__RSC_MANIFEST?.[srcPage]
Server ActionsloadManifestFromRelativePath(SERVER_REFERENCE_MANIFEST)maybeJSONParse(self.__RSC_SERVER_MANIFEST)

Sources: packages/next/src/server/route-modules/route-module.ts:275-306, packages/next/src/server/route-modules/route-module.ts:337-404

Warning

If prepare returns a falsy result, the handler immediately terminates execution by setting the response status code to 400, writing 'Bad Request', resolving any pending waitUntil promises, and returning null.

Sources: packages/next/src/build/templates/app-route.ts:139-144

Edge Route Module Adapter

Streaming and Response Lifecycle

The edge runtime integration wraps route modules via EdgeRouteModuleWrapper, bridging standard Web Request/Response types and Next.js internal execution contexts. When an edge app route is invoked, userland configuration and environmental settings such as __NEXT_BASE_PATH, __NEXT_I18N_CONFIG, and __NEXT_TRAILING_SLASH are supplied to the internal adapter handler.

export async function handler(
  request: Request,
  ctx: {
    waitUntil?: (prom: Promise<void>) => void
    signal?: AbortSignal
    requestMeta?: RequestMeta
  }
): Promise<Response> {
  const result = await internalHandler({
    request: {
      url: request.url,
      method: request.method,
      headers: toNodeOutgoingHttpHeaders(request.headers),
      nextConfig: {
        basePath: process.env.__NEXT_BASE_PATH,
        i18n: process.env.__NEXT_I18N_CONFIG as any,
        trailingSlash: Boolean(process.env.__NEXT_TRAILING_SLASH),
        experimental: {
          cacheLife: process.env.__NEXT_CACHE_LIFE as any,
          authInterrupts: Boolean(
            process.env.__NEXT_EXPERIMENTAL_AUTH_INTERRUPTS
          ),
          clientParamParsingOrigins: process.env
            .__NEXT_CLIENT_PARAM_PARSING_ORIGINS as any,
        },
      },
      page: {
        name: 'VAR_PAGE',
      },
      body:
        request.method !== 'GET' && request.method !== 'HEAD'
          ? (request.body ?? undefined)
          : undefined,
      waitUntil: ctx.waitUntil,
      requestMeta: ctx.requestMeta,
      signal: ctx.signal || new AbortController().signal,
    },
  })
 
  ctx.waitUntil?.(result.waitUntil)
 
  return result.response
}

Sources: packages/next/src/build/templates/edge-app-route.ts:42-84

Within EdgeRouteModuleWrapper.handler, dynamic route parameters are normalized from search parameters, cache handlers are initialized with configuration limits, and preview properties are retrieved via getEdgePreviewProps.

export function getEdgePreviewProps() {
  return {
    previewModeId: process.env.__NEXT_PREVIEW_MODE_ID || '',
    previewModeSigningKey: process.env.__NEXT_PREVIEW_MODE_SIGNING_KEY || '',
    previewModeEncryptionKey:
      process.env.__NEXT_PREVIEW_MODE_ENCRYPTION_KEY || '',
  }

Sources: packages/next/src/server/web/get-edge-preview-props.ts:6-12

Environment VariableSource Property / FallbackPurpose in Edge Context
__NEXT_PREVIEW_MODE_IDprocess.env.__NEXT_PREVIEW_MODE_IDIdentifies active draft/preview mode sessions
__NEXT_PREVIEW_MODE_SIGNING_KEYprocess.env.__NEXT_PREVIEW_MODE_SIGNING_KEYCryptographic signature validation for cookies
__NEXT_PREVIEW_MODE_ENCRYPTION_KEYprocess.env.__NEXT_PREVIEW_MODE_ENCRYPTION_KEYPayload encryption for preview cookies

Sources: packages/next/src/server/web/get-edge-preview-props.ts:6-12

Note

In the edge runtime, preview props are directly read from runtime environment variables injected by the edge-runtime or replaced during the edge build pipeline.

Sources: packages/next/src/server/web/get-edge-preview-props.ts:1-5

When handling responses, the wrapper manages asynchronous execution tasks and body streaming lifecycles. If a response body is absent, a deferred close controller timeout dispatches a close event. For streaming responses, trackStreamConsumed wraps the response body to guarantee close handlers execute correctly upon consumption.

  if (!res.body) {
    // we can delay running it until a bit later --
    // if it's needed, we'll have a `waitUntil` lock anyway.
    setTimeout(() => closeController.dispatchClose(), 0)
  } else {
    // NOTE: if this is a streaming response, onClose may be called later,
    // so we can't rely on `closeController.listeners` -- it might be 0 at this point.
    const trackedBody = trackStreamConsumed(res.body, () =>
      closeController.dispatchClose()
    )
    res = new Response(trackedBody, {
      status: res.status,
      statusText: res.statusText,
      headers: res.headers,
    })
  }
 
  return res
}

Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:158-176

Warning

Because streaming responses can invoke onClose at a later time, listeners cannot be tracked by a static count; trackStreamConsumed monitors body stream consumption to trigger closeController.dispatchClose().

Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:162-167

Pages API Route Resolution

Pages API Module Wrapper and Template

The PagesAPIRouteModule class manages execution for legacy Pages API routes. Upon instantiation, it validates that options.userland.default is a function and wraps the underlying apiResolver function using wrapApiHandler to assign the route name to root span attributes and trace execution under NodeSpan.runHandler.

constructor(options: PagesAPIRouteModuleOptions) {
  super(options)
 
  if (typeof options.userland.default !== 'function') {
    throw new Error(
      `Page ${options.definition.page} does not export a default function.`
    )
  }
 
  this.apiResolverWrapped = wrapApiHandler(
    options.definition.page,
    apiResolver
  )
}

Sources: packages/next/src/server/route-modules/pages-api/module.ts:115-128

The generated pages-api template exports the userland handler and config via hoisting, and executes a request handler function that attaches request metadata, handles Turbopack page normalization for /index routes, and calls routeModule.prepare(req, res, { srcPage }).

export default hoist(userland, 'default')
 
// Re-export config.
export const config = hoist(userland, 'config')
 
// Create and export the route module that will be consumed.
const routeModule = new PagesAPIRouteModule({
  definition: {
    kind: RouteKind.PAGES_API,
    page: 'VAR_DEFINITION_PAGE',
    pathname: 'VAR_DEFINITION_PATHNAME',
    // The following aren't used in production.
    bundlePath: '',
    filename: '',
  },
  userland,
  distDir: process.env.__NEXT_RELATIVE_DIST_DIR || '',
  relativeProjectDir: process.env.__NEXT_RELATIVE_PROJECT_DIR || '',
})
 
export async function handler(
  req: IncomingMessage,
  res: ServerResponse,
  ctx: {
    waitUntil?: (prom: Promise<void>) => void
    requestMeta?: RequestMeta
  }
) {
  if (ctx.requestMeta) {
    setRequestMeta(req, ctx.requestMeta)
  }
  if (routeModule.isDev) {
    addRequestMeta(req, 'devRequestTimingInternalsEnd', process.hrtime.bigint())
  }
  let srcPage = 'VAR_DEFINITION_PAGE'
 
  // turbopack doesn't normalize `/index` in the page name
  // so we need to to process dynamic routes properly
  // TODO: fix turbopack providing differing value from webpack
  if (process.env.TURBOPACK) {
    srcPage = srcPage.replace(/\/index$/, '') || '/'
  }
 
  const prepareResult = await routeModule.prepare(req, res, { srcPage })

Sources: packages/next/src/build/templates/pages-api.ts:27-70

Warning

If routeModule.prepare returns a falsy result, the template immediately responds with a 400 Bad Request status and terminates execution.

Sources: packages/next/src/build/templates/pages-api.ts:70-77

Node.js apiResolver Pipeline and Configuration

The apiResolver function orchestrates request parsing and execution. It reads the page configuration from resolverModule.config, extracting configuration properties that dictate body parsing behavior, response size limits, and external resolver status.

const config: PageConfig = resolverModule.config || {}
const bodyParser = config.api?.bodyParser !== false
const responseLimit = config.api?.responseLimit ?? true
const externalResolver = config.api?.externalResolver || false
 

Sources: packages/next/src/server/api-utils/node/api-resolver.ts:351-355

PageConfig API PropertyDefault Value / EvaluationDescription
config.api.bodyParsertrue (enabled unless explicitly false or object with false)Determines whether to parse the incoming request body.
config.api.responseLimittrue (defaults to RESPONSE_LIMIT_DEFAULT bytes)Enforces response content length limits and warns on overflow.
config.api.externalResolverfalseDisables unhandled resolution warnings for external routing middleware.

Sources: packages/next/src/server/api-utils/node/api-resolver.ts:352-354, packages/next/src/server/api-utils/index.ts:114-114

Note

apiResolver uses setLazyProp to lazily parse cookies, query parameters, preview data, and draft mode state on req only when accessed.

Sources: packages/next/src/server/api-utils/node/api-resolver.ts:356-375

Body Parsing and Response Helpers

When bodyParser is enabled and apiReq.body is not already populated, apiResolver invokes parseBody with the configured size limit, defaulting to '1mb'.

// Parsing of body
if (bodyParser && !apiReq.body) {
  apiReq.body = await parseBody(
    apiReq,
    config.api && config.api.bodyParser && config.api.bodyParser.sizeLimit
      ? config.api.bodyParser.sizeLimit
      : '1mb'
  )
}

Sources: packages/next/src/server/api-utils/node/api-resolver.ts:377-385

Response methods and helpers are monkey-patched or attached directly onto apiRes during resolution to provide utility methods.

Response Helper MethodImplementation TargetPurpose
res.status(statusCode)sendStatusCodeSets response status code and returns response object.
res.send(data)sendDataSends payload data back to the client.
res.json(data)sendJsonSerializes and sends JSON data.
res.redirect(statusOrUrl, url)redirectWrites redirection headers and terminates request.
res.setDraftMode(options)setDraftModeConfigures draft mode state cookies.
res.setPreviewData(data, options)setPreviewDataSets preview data cookies.
res.clearPreviewData(options)clearPreviewDataClears preview cookies using expiration dates in the past.
res.revalidate(urlPath, opts)revalidateTriggers on-demand revalidation for a path.

Sources: packages/next/src/server/api-utils/node/api-resolver.ts:410-426, packages/next/src/server/api-utils/index.ts:44-76, packages/next/src/server/api-utils/index.ts:119-164

Was this page helpful?