Next.js API Routes: Build Robust Serverless Endpoints
Next.js is renowned for its hybrid approach to rendering, seamlessly blending the best of static and server-side rendering. In addition to its Pages Router and App Router, Next.js offers a powerful mechanism to build backend endpoints directly within your application: API Routes. API Routes unlock a world of possibilities—from creating RESTful endpoints to handling webhooks—without leaving your Next.js project. In this article, we’ll dive deep into Next.js API Routes, exploring their folder structure, request/response helpers, configuration options, dynamic routing capabilities, advanced use cases, and best practices. By the end, you’ll have a comprehensive understanding of how to leverage API Routes to build robust, secure, and performant backend functionality in your Next.js applications.
Table of Contents
- What Are API Routes?
- Folder Structure and Conventions
- Basic Example
- Request Helpers
- Response Helpers
- Handling HTTP Methods
- Custom Configuration
- Dynamic and Catch-All Routes
- Streaming Responses
- CORS and Cross-Origin Considerations
- API Routes vs. Route Handlers
- Security and Validation
- Testing API Routes
- Best Practices and Performance Tips
- Conclusion
What Are API Routes?
API Routes in Next.js allow you to build server-side endpoints within the same project where you develop your front-end React components. When you create files under the pages/api directory, Next.js automatically maps them to corresponding HTTP endpoints under /api/*. These routes:
- Run on the server only: They are excluded from client-side bundles.
- Support all Node.js APIs: You have full access to the Node runtime, including file system, network, and environment variables.
- Integrate seamlessly with your Next.js app: Share utilities, environment variables, and code between your front-end and back-end.
API Routes simplify backend development by co-locating related code, reducing context switching between repositories, and enabling full-stack functionality within a single Next.js project.
Folder Structure and Conventions
By convention, any file placed in the pages/api directory is considered an API Route and mapped to /api/<filename>. Subdirectories within api translate to nested paths.
project-root/ ├── pages/ │ ├── index.tsx // Renders the home page │ └── api/ │ ├── hello.ts // GET /api/hello │ ├── users/ │ │ ├── index.ts // GET/POST /api/users │ │ └── [id].ts // GET/PUT/DELETE /api/users/:id │ └── post/ │ └── [...slug].ts// Catch-all /api/post/*
A few important notes:
- Server-only bundles: Files under pages/api are compiled only for server execution, meaning they won’t bloat your client bundle.
- Static exports incompatible: You cannot use next export on a project with API Routes.
- Page extensions: The pageExtensions setting in next.config.js applies to API Routes as well.
Basic Example
Let’s start with the simplest API Route. Create a file pages/api/hello.ts:
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next';
type ResponseData = {
message: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
res.status(200).json({ message: 'Hello from Next.js!' });
}
- NextApiRequest: Extends Node’s IncomingMessage, adding parsed cookies, query, and body.
- NextApiResponse: Extends Node’s ServerResponse with Express-like helpers (.status(), .json(), etc.).
- Response: JSON with { message: 'Hello from Next.js!' } and HTTP status 200.
Request Helpers
Next.js automatically parses incoming requests and exposes them through req:
Property Description
| req.cookies | An object of cookie names to values.
| req.query | An object representing the URL query string.
| req.body | The parsed request body (JSON, URL-encoded, etc.), or null if none was sent.
Under the hood, Next.js uses micro and raw-body to parse incoming requests. The default configuration supports JSON bodies of up to 1 MB.
Response Helpers
The res object in API Routes includes several convenience methods:
- res.status(code): Sets the HTTP status code.
- res.json(body): Serializes body to JSON and sets the appropriate headers.
- res.send(body): Sends a string, Buffer, or object (auto-serialized to JSON if object).
- res.redirect([status], url): Redirects to a new URL. Defaults to status 307.
- res.revalidate(path): Programmatically revalidate a page using getStaticProps.
These helpers streamline endpoint development:
export default async function handler(req, res) {
try {
const data = await fetchData();
res.status(200).json({ data });
} catch (error) {
res.status(500).json({ error: 'Internal Server Error' });
}
}
Handling HTTP Methods
API Routes receive every HTTP method by default. To distinguish behavior:
// pages/api/items.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case 'GET':
// Fetch items
return res.status(200).json({ items: [] });
case 'POST':
// Create a new item
return res.status(201).json({ message: 'Item created' });
default:
// Method not allowed
res.setHeader('Allow', ['GET', 'POST']);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
- req.method: The HTTP method string.
- Allow header: Recommended to list supported methods for clients.
Custom Configuration
Every API Route file can export a config object to override defaults:
export const config = {
api: {
bodyParser: {
sizeLimit: '500kb', // Maximum incoming body size
// set to false to disable parsing (e.g., raw streams)
},
externalResolver: true, // Suppress warnings when using connect/express
responseLimit: '8mb', // Warning threshold for response size
},
maxDuration: 10, // Max execution time in seconds (serverless)
};
- bodyParser.sizeLimit: Change the maximum size or disable parsing.
- externalResolver: Use when delegating to another framework’s handler.
- responseLimit: Warn if the response exceeds the given size.
- maxDuration: Control function timeout (serverless environments).
Dynamic and Catch-All Routes
Dynamic Routes
Create parameterized API endpoints by wrapping segment names in brackets:
pages/api/user/[id].ts → /api/user/:id
export default function handler(req, res) {
const { id } = req.query; // id is a string
res.status(200).end(`User ID: ${id}`);
}
Catch-All Routes
Match one or more nested segments:
pages/api/post/[...slug].ts → /api/post/a, /api/post/a/b, etc.
export default function handler(req, res) {
const { slug } = req.query; // slug is string[]
res.status(200).end(`Slug path: ${slug.join(', ')}`);
}
Optional Catch-All
Include [[...param]].ts to also match the root:
pages/api/post/[[...slug]].ts → /api/post, /api/post/a, /api/post/a/b
- For /api/post, req.query.slug is undefined or [].
- For /api/post/a, req.query.slug is ['a'].
Priority:
- Predefined static routes (create.ts)
- Dynamic ([id].ts)
- Catch-all ([...slug].ts)
Streaming Responses
While API Routes support streaming via res.write()/res.writeHead(), Next.js recommends transitioning to the App Router Route Handlers (Next.js 14+). However, if you need Server-Sent Events (SSE) or partial data:
// pages/api/stream.ts
import { NextApiResponse } from 'next';
export default async function handler(req, res: NextApiResponse) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-store',
Connection: 'keep-alive',
});
let counter = 0;
const interval = setInterval(() => {
res.write(`data: ${counter}\n\n`);
counter += 1;
if (counter === 10) {
clearInterval(interval);
res.end();
}
}, 1000);
}
- writeHead: Manually set headers.
- write: Stream chunks.
- end: Close the stream.
CORS and Cross-Origin Considerations
By default, API Routes are same-origin only. To enable CORS:
npm install cors
// pages/api/hello.ts
import Cors from 'cors';
import initMiddleware from '../../lib/init-middleware'; // helper to wrap middleware
import type { NextApiRequest, NextApiResponse } from 'next';
const cors = initMiddleware(
Cors({
methods: ['GET', 'POST', 'OPTIONS'],
origin: '*', // or specific domains
})
);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await cors(req, res);
res.json({ message: 'Hello with CORS!' });
}
- initMiddleware: Converts Express-style middleware to Next.js.
- Cors: Configurable origins, methods, credentials.
API Routes vs. Route Handlers
With the App Router, Route Handlers (app/api/.../route.ts) offer:
- Native support for streaming.
- Built-in Edge function compatibility.
- Ability to use React Server Components alongside handlers.
If you’re on Next.js 14+ and using the App Router, prefer Route Handlers. However, API Routes remain fully supported for existing Pages Router projects.
Security and Validation
Input Validation
Always validate and sanitize req.body, req.query, and req.cookies. Use libraries such as:
- Zod (Type-safe parsing)
- Joi
- Yup
import { z } from 'zod';
const schema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
export default function handler(req, res) {
try {
const data = schema.parse(req.body);
// Proceed with validated data
} catch (err) {
return res.status(400).json({ error: err.errors });
}
}
Authentication & Authorization
- JWT: Extract tokens from req.headers.authorization.
- Session Cookies: Use packages like next-auth or iron-session.
- API Keys: Check custom headers.
Testing API Routes
Use your preferred testing framework (Jest, Vitest) along with supertest:
// __tests__/api/hello.test.ts
import handler from '../../pages/api/hello';
import { createMocks } from 'node-mocks-http';
describe('/api/hello', () => {
it('returns 200 and message', async () => {
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(JSON.parse(res._getData())).toEqual({ message: 'Hello from Next.js!' });
});
});
- node-mocks-http: Mock req/res objects.
- Use teardown/setup hooks to manage environment variables or database connections.
Best Practices and Performance Tips
- Keep business logic separate
Abstract service functions outside API handlers to simplify testing and reuse. - Cache data when appropriate
Leverage res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate=120'). - Avoid blocking operations
Use async/await and non-blocking I/O. - Limit response size
Compress large payloads or paginate data. - Monitor cold starts
In serverless environments, bundle size and initialization time matter. - Leverage environment variables
Store secrets in .env.local and access via process.env.
Conclusion
Next.js API Routes deliver an elegant, integrated way to build backend APIs alongside your React front end. With first-class support for request parsing, response helpers, dynamic routing, and custom configuration, API Routes empower developers to create full-stack applications without context-switching between repositories or frameworks. As you grow, consider transitioning to the App Router’s Route Handlers for advanced streaming, edge compatibility, and React Server Component integration. Armed with the techniques and best practices in this article, you’re ready to architect robust, scalable, and secure APIs within your Next.js projects.
Happy coding!