How to Build an API in Next.js 14: Complete Guide to Pages Router Routes
The recommended way to handle API routes in a Next.js 14 project is to export a default async function from files inside the pages/api/ directory, which Next.js automatically wraps with OpenTelemetry tracing and executes through the internal apiResolver utility.
An api nextjs project leverages the Pages Router convention where every file under pages/api/ becomes a serverless endpoint. According to the vercel/next.js source code, the framework handles module validation, request/response augmentation, and error handling automatically through a dedicated pipeline in packages/next/src/server/route-modules/pages-api/module.ts.
How API Routes Work in Next.js 14
File-Based Routing Convention
Next.js treats every file under pages/api/ as a separate serverless function. When a request hits /api/hello, the framework:
- Maps the URL to the file (
pages/api/hello.ts) - Loads the module and validates that it exports a default function (validation logic in
packages/next/src/server/route-modules/pages-api/module.tslines 18-23) - Wraps the handler with
wrapApiHandlerto attach OpenTelemetry tracing spans (packages/next/src/server/api-utils/index.tslines 23-38) - Executes through
apiResolverwhich supplies body parsing, cookie handling, and response helpers (packages/next/src/server/api-utils/node/api-resolver.tslines 31-45)
Core Architecture Components
| Component | Purpose | Source Location |
|---|---|---|
| Page-API Module | Validates default export and executes handler via apiResolver |
packages/next/src/server/route-modules/pages-api/module.ts |
wrapApiHandler |
Adds OpenTelemetry tracing (next.route) around requests |
packages/next/src/server/api-utils/index.ts |
apiResolver |
Augments request/response objects, handles body parsing, enforces response limits | packages/next/src/server/api-utils/node/api-resolver.ts |
Recommended Practices for API Next.js Routes
Export a single async function as the default from your API files. The function receives typed NextApiRequest and NextApiResponse objects that include framework-provided helpers.
Rely on the built-in body parser enabled by default. Disable it only when handling raw streams:
export const config = {
api: { bodyParser: false }
}
Use the response helpers (res.status, res.json, res.redirect, res.revalidate) attached by apiResolver. These guarantee proper header handling and respect the default 2 MB response-size limit (configurable via api.responseLimit).
Throw ApiError for intentional HTTP error responses. The resolver catches these and sends appropriate status codes without exposing stack traces in production.
Never return a value from the handler. Next.js expects the handler to end the response itself. Returning a value triggers a warning (see the warning logic at line 38 of apiResolver).
Code Examples
Basic Hello World Endpoint
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default async (_req: NextApiRequest, res: NextApiResponse) => {
// res.json is provided by the framework
res.json({ message: 'Hello, world!' })
}
This matches the minimal example used in the repository's test suite at test/production/graceful-shutdown/src/pages/api/fast.ts.
POST Handler with Validation
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { ApiError } from 'next/dist/server/api-utils'
export const config = {
api: { bodyParser: { sizeLimit: '500kb' } }
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method Not Allowed' })
}
const { name, email } = req.body ?? {}
if (!name || !email) {
throw new ApiError(400, 'Missing name or email')
}
const user = { id: Date.now(), name, email }
res.status(201).json(user)
}
The apiResolver attaches req.body via internal parseBody and provides res.status/res.json helpers (lines 78-85 of api-resolver.ts).
On-Demand Revalidation and Preview Mode
// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default async (req: NextApiRequest, res: NextApiResponse) => {
// Enable preview mode with secure cookies
res.setPreviewData({ foo: 'bar' }, { maxAge: 60 * 60 })
// Revalidate a static page
await res.revalidate('/posts/123')
res.json({ revalidated: true })
}
res.setPreviewData, res.revalidate, and res.setDraftMode are attached in apiResolver (lines 15-21).
Redirects from API Routes
// pages/api/redirect.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default async (_req: NextApiRequest, res: NextApiResponse) => {
// Permanent redirect (301) to external URL
res.redirect(301, 'https://example.com')
}
The redirect helper validates arguments and writes the proper Location header (see api-utils lines 59-73).
Summary
- File-based routing: Place files under
pages/api/to create serverless endpoints automatically. - Default export: Export a single async function receiving
NextApiRequestandNextApiResponse. - Built-in helpers: Leverage
res.status,res.json,res.redirect, andres.revalidateattached by theapiResolverinpackages/next/src/server/api-utils/node/api-resolver.ts. - Error handling: Throw
ApiErrorfor controlled HTTP errors; the framework hides stack traces in production. - Configuration: Use
export const configto customize body parsing and response limits.
Frequently Asked Questions
How do I disable the default body parser in a Next.js API route?
Add a config export with bodyParser: false. This is required when handling raw request streams or implementing custom parsers like GraphQL or tRPC middleware.
export const config = {
api: { bodyParser: false }
}
What is the maximum response size for API routes in Next.js?
The default limit is 2 MB. You can configure this via the responseLimit option in your route config, or set it to false to disable the check entirely. The enforcement logic resides in packages/next/src/server/api-utils/node/api-resolver.ts.
Can I return a value from my API route handler?
No. Next.js expects the handler to terminate the response using res.end(), res.json(), or similar methods. Returning a value from the async function triggers a runtime warning and can lead to unexpected behavior, as noted in the resolver logic at line 38 of api-resolver.ts.
How does Next.js handle errors thrown in API routes?
The apiResolver catches errors automatically. If you throw an ApiError (imported from next/dist/server/api-utils), it sends the specified status code and message without leaking stack traces in production. Unhandled errors return a 500 status code with generic messaging to prevent information disclosure.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →