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 Handlers in Next.js manage backend data endpoints and request dispatching across distinct execution runtimes and application models. They bridge incoming HTTP traffic with userland route logic, balancing modern Web API Request and Response primitives in App Routes with legacy Node.js message handlers in Pages API routes. By orchestrating payload parsing, header adaptation, security checks, and prerender state tracking, these server modules handle request lifecycles uniformly across environments. Sources: packages/next/src/server/route-modules/app-route/module.ts:793-978, packages/next/src/server/api-utils/node/api-resolver.ts:331-489, packages/next/src/server/web/edge-route-module-wrapper.ts:84-177
App Route modules coordinate the complete lifecycle of incoming HTTP requests for App Router endpoints, managing asynchronous module loading, HTTP method resolution, dynamic bailout checks, and response validation. When a request arrives, the route module initializes request storage, evaluates static generation constraints, and executes the target userland handler within nested AsyncLocalStorage contexts. Sources: packages/next/src/server/route-modules/app-route/module.ts:793-978
The execution pipeline processes incoming requests through a strict sequence of validation, store initialization, and tracer spans before invoking the userland handler.
AppRouteRouteModule.handle() → this.ensureUserland() → resolveHandlerFromUserland() or resolveHandler() → getImplicitTags() → createRequestStoreForAPI() → createWorkStore() → actionAsyncStorage.run() → workUnitAsyncStorage.run() → workAsyncStorage.run() → tracer.trace() → this.do() Sources: packages/next/src/server/route-modules/app-route/module.ts:793-952
During this flow, ensureUserland() guarantees that modules utilizing top-level await are fully resolved before execution. Next, handle() checks whether non-static methods are present and applies dynamic configuration rules. Sources: packages/next/src/server/route-modules/app-route/module.ts:793-878
Incoming HTTP methods are normalized and matched against exported userland handlers using resolveHandler(method: string) or resolveHandlerFromUserland(). To prevent Remote Code Execution (RCE), requests with unrecognized HTTP methods are intercepted immediately. Sources: packages/next/src/server/route-modules/app-route/module.ts:391-396, packages/next/src/server/route-modules/app-route/module.ts:813-816
Sources: packages/next/src/server/route-modules/app-route/module.ts:391-396, packages/next/src/server/route-modules/app-route/module.ts:807-816
App Route modules evaluate static generation options via export const dynamic configurations. The route execution runtime inspects the dynamic mode and modifies the incoming request object or throws a DynamicServerError when static generation rules are violated. Sources: packages/next/src/server/route-modules/app-route/module.ts:869-926
Warning
Exporting non-static HTTP methods such as POST, PUT, DELETE, PATCH, or OPTIONS via hasNonStaticMethods() will automatically trigger a DynamicServerError if the route is evaluated during static generation. Sources: packages/next/src/server/route-modules/app-route/module.ts:866-878, packages/next/src/server/route-modules/app-route/module.ts:990-999
The EdgeRouteModuleWrapper class adapts an AppRouteRouteModule for execution inside Edge runtimes, managing request parsing, dynamic route parameter normalization, cache handler initialization, and streaming response body consumption via CloseController. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:35-50
When an Edge request arrives, EdgeRouteModuleWrapper.wrap() instantiates the wrapper and returns an EdgeHandler adapter function. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:61-82
The execution request proceeds through the internal handler pipeline:
EdgeRouteModuleWrapper.handler() → getServerUtils() → routeModule.getNextConfigEdge() → initializeCacheHandlers() → normalizeDynamicRouteParams() → routeModule.handle() → trackStreamConsumed() / closeController.dispatchClose(). Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:84-176
getServerUtils() builds URL matching utilities using matcher.isDynamic and matcher.definition.pathname. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:88-96routeModule.getNextConfigEdge() reads configuration settings for cache limits and life profiles. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:98-100initializeCacheHandlers() and setCacheHandler() configure runtime memory limits and custom cache adapters. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:101-104normalizeDynamicRouteParams() parses search parameters into dynamic route parameters. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:106-109routeModule.handle() executes userland handler logic using constructed context parameters. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:116-151Note
If a response has no body, setTimeout() triggers closeController.dispatchClose() asynchronously. For streaming responses, trackStreamConsumed() wraps res.body to invoke closeController.dispatchClose() upon consumption. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:159-174
The AppRouteRouteHandlerContext passed to routeModule.handle() configures runtime rendering options and feature flags specifically tailored for Edge environments. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:116-148
Caution
Both useCacheTimeout and staticPageGenerationTimeout are hardcoded to 0 in Edge route contexts because Cache Components and static generation are unsupported in the Edge runtime. If invoked, they act as sentinels to immediately surface unexpected access errors. Sources: packages/next/src/server/web/edge-route-module-wrapper.ts:131-143
The legacy Pages API infrastructure bridges traditional Node.js request-response lifecycles and Next.js route handling via PagesAPIRouteModule and apiResolver. When a Pages API request is processed, PagesAPIRouteModule.render() initializes performance tracing through wrapApiHandler() and delegates directly to apiResolver(), supplying the userland module, context properties, and error callbacks. Sources: packages/next/src/server/api-utils/index.ts:22-37, packages/next/src/server/route-modules/pages-api/module.ts:115-161
The resolution flow executes through a deterministic pipeline from module instantiation down to userland handler invocation and telemetry reporting.
PagesAPIRouteModule constructor → wrapApiHandler() → PagesAPIRouteModule.render() → apiResolver() → parseBody() → resolver(req, res) → onError?.() Sources: packages/next/src/server/api-utils/index.ts:22-37, packages/next/src/server/api-utils/node/api-resolver.ts:331-489, packages/next/src/server/route-modules/pages-api/module.ts:115-161
PagesAPIRouteModule validates that options.userland.default is a function and wraps apiResolver using wrapApiHandler() to establish root span attributes and trace execution under NodeSpan.runHandler. Sources: packages/next/src/server/api-utils/index.ts:22-37, packages/next/src/server/route-modules/pages-api/module.ts:115-128apiResolver extracts page configuration (resolverModule.config), attaches lazy cookie parsers, defines writable req.query properties to support Express 5 compatibility, and configures preview data getters. Sources: packages/next/src/server/api-utils/node/api-resolver.ts:351-376bodyParser is enabled and apiReq.body is unparsed, parseBody() processes the payload. Meanwhile, apiRes.write and apiRes.end are monkey-patched to track cumulative content length against responseLimit. Sources: packages/next/src/server/api-utils/node/api-resolver.ts:377-409(req, res). If execution throws an ApiError or unhandled exception, onError telemetry is notified, and appropriate error responses are dispatched based on dev and propagateError flags. Sources: packages/next/src/server/api-utils/node/api-resolver.ts:428-488Warning
Returning a Web API Response object from a Pages API route in the Node.js runtime throws an explicit error instructing developers to use runtime: "edge" instead. Sources: packages/next/src/server/api-utils/node/api-resolver.ts:439-444
API routes export a configuration object (PageConfig) that dictates runtime behavior inside apiResolver.
| Configuration Key | Type | Default Value | Purpose / Behavior | Sources |
|-------------------|------|---------------+--------------------+---------|
| api.bodyParser | boolean \| { sizeLimit?: string \| number } | true | Controls automatic JSON/urlencoded body parsing; set to false to consume raw streams | packages/next/src/server/api-utils/node/api-resolver.ts:352-352 |
| api.responseLimit | boolean \| string | true (4MB) | Configures the maximum response size payload before logging a performance warning | packages/next/src/server/api-utils/node/api-resolver.ts:353-353 |
| api.externalResolver | boolean | false | Flags whether another middleware or external library handles response termination, suppressing stalled-request warnings | packages/next/src/server/api-utils/node/api-resolver.ts:354-354 |
Sources: packages/next/src/server/api-utils/node/api-resolver.ts:357-454, packages/next/src/server/api-utils/index.ts:211-231
Pages API request ingestion and body processing coordinate through apiResolver and parseBody to read incoming streams, enforce size limits, and parse payloads according to content-type headers. Sources: packages/next/src/server/api-utils/node/api-resolver.ts:377-385, packages/next/src/server/api-utils/node/parse-body.ts:29-66
apiResolver: Evaluates page configuration to check if bodyParser is enabled (config.api?.bodyParser !== false). If enabled and apiReq.body is unpopulated, it invokes parseBody with the configured size limit or defaults to '1mb'. Sources: packages/next/src/server/api-utils/node/api-resolver.ts:351-352, packages/next/src/server/api-utils/node/api-resolver.ts:378-384parseBody: Parses the content-type header, extracts the character set encoding (defaulting to utf-8), and reads the raw request buffer via getRawBody up to the specified size limit. It converts the buffer to a string and branches based on the media type, handing JSON bodies over to parseJson. Sources: packages/next/src/server/api-utils/node/parse-body.ts:29-65parseJson: Inspects the string length. If empty, it returns an empty object {} as a special-case fallback for client-side mistakes; otherwise, it executes JSON.parse. Sources: packages/next/src/server/api-utils/node/parse-body.ts:12-23ApiError: Thrown when parsing fails or payload sizes exceed limits. Entity size errors throw an ApiError with status 413 (Body exceeded limit), malformed JSON throws status 400 (Invalid JSON), and general body errors throw status 400 (Invalid body). Sources: packages/next/src/server/api-utils/node/parse-body.ts:21-22, packages/next/src/server/api-utils/node/parse-body.ts:49-53Sources: packages/next/src/server/api-utils/node/api-resolver.ts:377-385, packages/next/src/server/api-utils/node/parse-body.ts:12-66, packages/next/src/server/api-utils/index.ts:176-183
| Media Type Match | Parsing Mechanism | Return Output | Sources |
|------------------|-------------------|---------------+---------|
| application/json, application/ld+json | parseJson() via JSON.parse | Object / Parsed JSON | packages/next/src/server/api-utils/node/parse-body.ts:58-59 |
| application/x-www-form-urlencoded | querystring.decode() | Key-value dictionary | packages/next/src/server/api-utils/node/parse-body.ts:60-62 |
| Other types (fallback) | Raw string conversion | string | packages/next/src/server/api-utils/node/parse-body.ts:63-65 |
Warning
If raw-body throws an error where e.type equals 'entity.too.large', parseBody catches it and raises an ApiError with status code 413. Any other failure yields status code 400 with message 'Invalid body'. Sources: packages/next/src/server/api-utils/node/parse-body.ts:48-54
Preview data resolution and request header adapters handle the decryption of preview cookies, evaluation of on-demand revalidation flags, and normalization of Node.js raw request headers into web-standard interfaces. The apiResolver function initializes lazy properties on the incoming request object for cookies, query parameters, preview data, and draft mode state. When previewData is accessed, it invokes tryGetPreviewData, which inspects incoming headers to determine if an on-demand revalidation request is underway. Sources: packages/next/src/server/api-utils/node/api-resolver.ts:356-376, packages/next/src/server/api-utils/node/try-get-preview-data.ts:17-30
apiResolver sets up lazy evaluation for previewData by calling tryGetPreviewData(req, res, apiContext, !!apiContext.multiZoneDraftMode). Sources: packages/next/src/server/api-utils/node/api-resolver.ts:367-369tryGetPreviewData inspects request headers by calling checkIsOnDemandRevalidate(req.headers, options). Sources: packages/next/src/server/api-utils/node/try-get-preview-data.ts:25-28checkIsOnDemandRevalidate verifies whether rawHeaders.get exists, and if so, invokes HeadersAdapter.from(rawHeaders) to wrap standard headers. Sources: packages/next/src/server/api-utils/index.ts:86-87HeadersAdapter.from checks if the input is already an instance of Headers; if it is a plain IncomingHttpHeaders object, it instantiates and returns a new HeadersAdapter. Sources: packages/next/src/server/web/spec-extension/adapters/headers.ts:155-159checkIsOnDemandRevalidate subsequently invokes .get() on the header collection, which calls HeadersAdapter.prototype.get to retrieve header values, executing this.merge(value) if multiple values exist as an array. Sources: packages/next/src/server/api-utils/index.ts:89-90, packages/next/src/server/web/spec-extension/adapters/headers.ts:143-147, packages/next/src/server/web/spec-extension/adapters/headers.ts:176-181Sources: packages/next/src/server/api-utils/node/api-resolver.ts:367-369, packages/next/src/server/api-utils/node/try-get-preview-data.ts:25-28, packages/next/src/server/api-utils/index.ts:86-87, packages/next/src/server/web/spec-extension/adapters/headers.ts:143-147, packages/next/src/server/web/spec-extension/adapters/headers.ts:155-159, packages/next/src/server/web/spec-extension/adapters/headers.ts:176-181
Sources: packages/next/src/server/api-utils/index.ts:7-8, packages/next/src/server/api-utils/index.ts:111-117
Sources: packages/next/src/server/api-utils/node/try-get-preview-data.ts:34-36, packages/next/src/server/api-utils/node/try-get-preview-data.ts:68-82, packages/next/src/server/api-utils/node/try-get-preview-data.ts:111-114, packages/next/src/server/web/spec-extension/adapters/headers.ts:36-114
Warning
If an on-demand revalidation request is detected via checkIsOnDemandRevalidate, tryGetPreviewData immediately short-circuits and returns false, disabling preview mode for that request to prevent revalidation requests from executing under user preview contexts. Sources: packages/next/src/server/api-utils/node/try-get-preview-data.ts:25-30
Caution
If only one of the two preview cookies (COOKIE_NAME_PRERENDER_BYPASS or COOKIE_NAME_PRERENDER_DATA) is present, tryGetPreviewData invokes clearPreviewData(res) unless multiZoneDraftMode is enabled, wiping both cookies from the response headers. Sources: packages/next/src/server/api-utils/node/try-get-preview-data.ts:68-74
During static generation or build-time export, Route Handlers (app-route) must be orchestrated to serialize their output bodies, metadata, and revalidation parameters into designated files via the multi-file writer. This orchestration coordinates request adaptation, module loading, static generation validation, execution error handling, and header serialization.
The export orchestration for app route modules processes requests through a sequence of normalization, validation, and storage execution steps:
exportAppRoute() → module.ensureUserland() → isStaticGenEnabled() → module.handle() → afterRunner.executeAfter() → fileWriter.append()
exportAppRoute(): Initializes the absolute request URL, wraps the node request using NextRequestAdapter.fromNodeNextRequest, and instantiates an AfterRunner and AppRouteRouteHandlerContext.
Sources: packages/next/src/export/routes/app-route.ts:58-98
module.ensureUserland(): Ensures that asynchronous modules (including those with top-level await) are fully resolved before the route handler is invoked.
Sources: packages/next/src/export/routes/app-route.ts:101-105
isStaticGenEnabled(): Inspects the loaded userland module to check if static generation is permitted, bypassing this check if the route is a metadata route or if cacheComponents is active.
Sources: packages/next/src/export/routes/app-route.ts:106-122
module.handle(): Dispatches the request through the handler pipeline, verifying that the returned value is a valid Response object and collecting revalidation tags and times into renderOpts.
Sources: packages/next/src/server/route-modules/app-route/module.ts:749-791, packages/next/src/export/routes/app-route.ts:124-124
afterRunner.executeAfter(): Executes any deferred callbacks registered during the route handler lifecycle prior to writing out final binary blobs and metadata.
Sources: packages/next/src/export/routes/app-route.ts:131-135
fileWriter.append(): Serializes the response body into a _body suffix file and writes response headers and status codes into a _meta suffix file.
Sources: packages/next/src/export/routes/app-route.ts:161-169
Caution
If a route handler returns a response status code greater than or equal to 400 (except for status 404), exportAppRoute immediately intercepts the response and returns a revalidation value of 0, preventing erroneous error pages from being cached as static output. Sources: packages/next/src/export/routes/app-route.ts:126-129
Warning
Route Handlers enforce strict return verification: if a handler resolves without returning a valid instance of the web Response object, AppRouteRouteModule.handle throws an explicit error indicating that a Response or NextResponse must be returned across all execution branches. Sources: packages/next/src/server/route-modules/app-route/module.ts:750-765