Next.js API Routes: Build Robust Serverless Endpoints

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

  1. What Are API Routes?
  2. Folder Structure and Conventions
  3. Basic Example
  4. Request Helpers
  5. Response Helpers
  6. Handling HTTP Methods
  7. Custom Configuration
  8. Dynamic and Catch-All Routes
  9. Streaming Responses
  10. CORS and Cross-Origin Considerations
  11. API Routes vs. Route Handlers
  12. Security and Validation
  13. Testing API Routes
  14. Best Practices and Performance Tips
  15. 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:

  1. Predefined static routes (create.ts)
  2. Dynamic ([id].ts)
  3. 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

  1. Keep business logic separate
    Abstract service functions outside API handlers to simplify testing and reuse.
  2. Cache data when appropriate
    Leverage res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate=120').
  3. Avoid blocking operations
    Use async/await and non-blocking I/O.
  4. Limit response size
    Compress large payloads or paginate data.
  5. Monitor cold starts
    In serverless environments, bundle size and initialization time matter.
  6. 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!


#Next.js #API Routes #Next.js API #serverless endpoints #dynamic API #request helpers #response helpers #CORS #streaming responses #NextApiRequest #NextApiResponse #route handlers #edge functions

Subscribe to our newsletter

12k+

Subscribers

Weekly

Frequency

Free

Always