Relevant source files
The following files were used as context for generating this wiki page:
- packages/next/src/server/route-modules/app-route/module.ts
- packages/next/src/build/templates/app-route.ts
- packages/next/src/build/templates/app-page-runtime.ts
- packages/next/src/server/app-render/action-handler.ts
- packages/next/src/build/templates/pages-api.ts
- packages/next/src/server/route-modules/pages/pages-handler.ts
- packages/next/src/build/templates/edge-ssr-app.ts
- packages/next/src/server/base-server.ts
- packages/next/src/server/next-server.ts
- packages/next/src/server/web/edge-route-module-wrapper.ts
- packages/next/src/server/api-utils/node/api-resolver.ts
- packages/next/src/build/templates/edge-app-route.ts
- packages/next/src/server/dev/next-dev-server.ts
- packages/next/src/server/lib/router-server.ts
- packages/next/src/server/web/adapter.ts
- packages/next/src/build/templates/edge-ssr.ts
- packages/next/src/server/route-modules/pages-api/module.ts
- packages/next/src/server/api-utils/index.ts
- packages/next/src/server/route-modules/app-page/module.ts
- packages/next/src/server/lib/patch-set-header.ts
- packages/next/src/server/request-meta.ts
- packages/next/src/server/route-modules/route-module.ts
- packages/next/src/server/web/get-edge-preview-props.ts
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.
-
handler: The template entry function receives the incoming request and response objects, attaches request metadata if provided, and invokesrouteModule.prepare(). Sources: packages/next/src/build/templates/app-route.ts:106-137 -
prepare: Traces the preparation span using the tracer utility and delegates the execution toprepareImpl(). Sources: packages/next/src/server/route-modules/route-module.ts:635-648 -
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 -
patchSetHeaderWithCookieSupport: Wraps the responsesetHeadermethod to interceptset-cookieheaders, 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 -
getRequestMeta: Retrieves internal request metadata properties such asmiddlewareCookie,relativeProjectDir, anddistDirfrom 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 Mode | Store Property Mutated | Request Modification |
|---|---|---|
force-dynamic | workStore.forceDynamic = true | None (Standard NextRequest) |
force-static | workStore.forceStatic = true | new Proxy(req, forceStaticRequestHandlers) |
error | workStore.dynamicShouldError = true | None (Standard NextRequest) |
auto / undefined | None | proxyNextRequest(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 Category | Node Runtime Source / Method | Edge Runtime Source / Fallback |
|---|---|---|
| Routes Manifest | loadManifestFromRelativePath(ROUTES_MANIFEST) | process.env.__NEXT_REWRITES / defaults |
| Prerender Manifest | loadManifestFromRelativePath(PRERENDER_MANIFEST) | Static empty routes object |
| Client Reference | evalManifestFromRelativePath(...) | self.__RSC_MANIFEST?.[srcPage] |
| Server Actions | loadManifestFromRelativePath(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 Variable | Source Property / Fallback | Purpose in Edge Context |
|---|---|---|
__NEXT_PREVIEW_MODE_ID | process.env.__NEXT_PREVIEW_MODE_ID | Identifies active draft/preview mode sessions |
__NEXT_PREVIEW_MODE_SIGNING_KEY | process.env.__NEXT_PREVIEW_MODE_SIGNING_KEY | Cryptographic signature validation for cookies |
__NEXT_PREVIEW_MODE_ENCRYPTION_KEY | process.env.__NEXT_PREVIEW_MODE_ENCRYPTION_KEY | Payload 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 Property | Default Value / Evaluation | Description |
|---|---|---|
config.api.bodyParser | true (enabled unless explicitly false or object with false) | Determines whether to parse the incoming request body. |
config.api.responseLimit | true (defaults to RESPONSE_LIMIT_DEFAULT bytes) | Enforces response content length limits and warns on overflow. |
config.api.externalResolver | false | Disables 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 Method | Implementation Target | Purpose |
|---|---|---|
res.status(statusCode) | sendStatusCode | Sets response status code and returns response object. |
res.send(data) | sendData | Sends payload data back to the client. |
res.json(data) | sendJson | Serializes and sends JSON data. |
res.redirect(statusOrUrl, url) | redirect | Writes redirection headers and terminates request. |
res.setDraftMode(options) | setDraftMode | Configures draft mode state cookies. |
res.setPreviewData(data, options) | setPreviewData | Sets preview data cookies. |
res.clearPreviewData(options) | clearPreviewData | Clears preview cookies using expiration dates in the past. |
res.revalidate(urlPath, opts) | revalidate | Triggers 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
Related
On This Page
OverviewRoute Module Base ArchitectureExecution Walkthrough and SequenceApp Route Module ExecutionDynamic Dispatch and HTTP Method ResolutionExecution Lifecycle and Dynamic Behavior ModesNode App Route InvocationRequest Normalization and Manifest CachingResponse Wrapping and Execution FlowEdge Route Module AdapterStreaming and Response LifecyclePages API Route ResolutionPages API Module Wrapper and TemplateNode.js apiResolver Pipeline and ConfigurationBody Parsing and Response HelpersRelated