Skip to content
Back to Blog

Friday, February 28th 2025

Building APIs with Next.js

Posted by

This guide will cover how you can build APIs with Next.js, including setting up your project, understanding the App Router and Route Handlers, handling multiple HTTP methods, implementing dynamic routing, creating reusable middleware logic, and deciding when to spin up a dedicated API layer.

1. Getting started

1.1 Create a Next.js app

If you’re starting fresh, you can create a new Next.js project using:

Terminal
npx create-next-app@latest --api

Note: The --api flag automatically includes an example route.ts in your new project’s app/ folder, demonstrating how to create an API endpoint.

1.2 App Router vs. Pages Router

  • Pages Router: Historically, Next.js used pages/api/* for APIs. This approach relied on Node.js request/response objects and an Express-like API.
  • App Router (Default): Introduced in Next.js 13, the App Router fully embraces web standard Request/Response APIs. Instead of pages/api/*, you can now place route.ts or route.js files anywhere inside the app/ directory.

Why switch? The App Router’s “Route Handlers” lean on the Web Platform Request/Response APIs rather than Node.js-specific APIs. This simplifies learning, reduces friction, and helps you reuse your knowledge across different tools.

2. Why (and when) to build APIs with Next.js

  1. Public API for Multiple Clients

    • You can build a public API that’s consumed by your Next.js web app, a separate mobile app, or any third-party service. For example, you might fetch from /api/users both in your React website and a React Native mobile app.
  2. Proxy to an Existing Backend

    • Sometimes you want to hide or consolidate external microservices behind a single endpoint. Next.js Route Handlers can act as a proxy or middle layer to another existing backend. For instance, you might intercept requests, handle authentication, transform data, and then pass the request along to an upstream API.
  3. Webhooks and Integrations

    • If you receive external callbacks or webhooks (e.g., from Stripe, GitHub, Twilio), you can handle them with Route Handlers.
  4. Custom Authentication

    • If you need sessions, tokens, or other auth logic, you can store cookies, read headers, and respond with the appropriate data in your Next.js API layer.

Note: If you only need server-side data fetching for your own Next.js app (and you don’t need to share that data externally), Server Components might be sufficient to fetch data directly during render—no separate API layer is required.

3. Creating an API with Route Handlers

3.1 Basic file setup

In the App Router (app/), create a folder that represents your route, and inside it, a route.ts file.

For example, to create an endpoint at /api/users:

app
└── api
    └── users
        └── route.ts

3.2 Multiple HTTP methods in one file

Unlike the Pages Router API routes (which had a single default export), you can export multiple functions representing different HTTP methods from the same file.

app/api/users/route.ts
import { NextRequest } from 'next/server';
 
export async function GET(request: NextRequest) {
  // For example, fetch data from your DB here
  const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
  return new Response(JSON.stringify(users), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  });
}
 
export async function POST(request: NextRequest) {
  // Parse the request body
  const body = await request.json();
  const { name } = body;
 
  // e.g. Insert new user into your DB
  const newUser = { id: Date.now(), name };
 
  return new Response(JSON.stringify(newUser), {
    status: 201,
    headers: { 'Content-Type': 'application/json' },
  });
}

Now, sending a GET request to /api/users returns your list of users, while a POST request to the same URL will insert a new one.

4. Working with Web APIs

4.1 Directly using Request & Response

By default, your Route Handler methods (GET, POST, etc.) receive a standard Request object, and you must return a standard Response object.

4.2 Query parameters

app/api/search/route.ts
import { NextRequest } from 'next/server';
 
export function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const query = searchParams.get('query'); // e.g. `/api/search?query=hello`
 
  return new Response(
    JSON.stringify({ result: `You searched for: ${query}` }),
    {
      headers: { 'Content-Type': 'application/json' },
    },
  );
}

4.3 Headers and cookies

app/api/auth/route.ts
import { NextRequest } from 'next/server';
import { cookies, headers } from 'next/headers';
 
export async function GET(request: NextRequest) {
  // 1. Using 'next/headers' helpers
  const cookieStore = cookies();
  const token = cookieStore.get('token');
 
  const headersList = headers();
  const referer = headersList.get('referer');
 
  // 2. Using the standard Web APIs
  const userAgent = request.headers.get('user-agent');
 
  return new Response(JSON.stringify({ token, referer, userAgent }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

The cookies() and headers() functions can be helpful if you plan to re-use shared logic across other server-side code in Next.js.

5. Dynamic routes

To create dynamic paths (e.g. /api/users/:id), use Dynamic Segments in your folder structure:

app
└── api
    └── users
        └── [id]
            └── route.ts

This file corresponds to a URL like /api/users/123, with the 123 captured as a parameter.

app/api/users/[id]/route.ts
import { NextRequest } from 'next/server';
 
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const id = (await params).id;
  // e.g. Query a database for user with ID `id`
  return new Response(JSON.stringify({ id, name: `User ${id}` }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  });
}
 
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const id = (await params).id;
  // e.g. Delete user with ID `id` in DB
  return new Response(null, { status: 204 });
}

Here, params.id gives you the dynamic segment.

6. Using Next.js as a proxy or forwarding layer

A common scenario is proxying an existing backend service. You can authenticate requests, handle logging, or transform data before sending it to a remote server or backend:

app/api/external/route.ts
import { NextRequest } from 'next/server';
 
export async function GET(request: NextRequest) {
  const response = await fetch('https://example.com/api/data', {
    // Optional: forward some headers, add auth tokens, etc.
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
  });
 
  // Transform or forward the response
  const data = await response.json();
  const transformed = { ...data, source: 'proxied-through-nextjs' };
 
  return new Response(JSON.stringify(transformed), {
    headers: { 'Content-Type': 'application/json' },
  });
}

Now your clients only need to call /api/external, and Next.js will handle the rest. This is also sometimes called a “Backend for Frontend” or BFF.

7. Building shared “middleware” logic

If you want to apply the same logic (e.g. authentication checks, logging) across multiple Route Handlers, you can create reusable functions that wrap your handlers:

app/lib/with-auth.ts
import { NextRequest } from 'next/server';
 
type Handler = (req: NextRequest, context?: any) => Promise<Response>;
 
export function withAuth(handler: Handler): Handler {
  return async (req, context) => {
    const token = req.cookies.get('token')?.value;
    if (!token) {
      return new Response(JSON.stringify({ error: 'Unauthorized' }), {
        status: 401,
        headers: { 'Content-Type': 'application/json' },
      });
    }
 
    // If authenticated, call the original handler
    return handler(req, context);
  };
}

Then in your Route Handler:

app/api/secret/route.ts
import { NextRequest } from 'next/server';
import { withAuth } from '@/app/lib/with-auth';
 
async function secretGET(request: NextRequest) {
  return new Response(JSON.stringify({ secret: 'Here be dragons' }), {
    headers: { 'Content-Type': 'application/json' },
  });
}
 
export const GET = withAuth(secretGET);

8. Deployment and “SPA Mode” considerations

8.1 Standard Node.js deployment

The standard Next.js server deployment using next start enables you to use features like Route Handlers, Server Components, Middleware and more – while taking advantage of dynamic, request time information.

There is no additional configuration required. See Deploying for more details.

8.2 SPA/Static Export

Next.js also supports outputting your entire site as a static Single-Page Application (SPA).

You can enable this by setting:

next.config.ts
import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  output: 'export',
};
 
export default nextConfig;

In static export mode, Next.js will generate purely static HTML, CSS, and JS. You cannot run server-side code (like API endpoints). If you still need an API, you’d have to host it separately (e.g., a standalone Node.js server).

Note:

  • GET Route Handlers can be statically exported if they don’t rely on dynamic request data. They become static files in your out folder.
  • All other server features (dynamic requests, rewriting cookies, etc.) are not supported in a pure SPA export.

8.3 Deploying APIs on Vercel

If you are deploying your Next.js application to Vercel, we have a guide on deploying APIs. This includes other Vercel features like programmatic rate-limiting through the Vercel Firewall. Vercel also offers Cron Jobs, which are commonly needed with API approaches.

9. When to skip creating an API endpoint

With the App Router’s React Server Components, you can fetch data directly on the server without exposing a public endpoint:

app/users/page.tsx
// (Server Component)
export default async function UsersPage() {
  // This fetch runs on the server (no client-side code needed here)
  const res = await fetch('https://api.example.com/users');
  const data = await res.json();
 
  return (
    <main>
      <h1>Users</h1>
      <ul>
        {data.map((user: any) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </main>
  );
}

If your data is only used inside your Next.js app, you may not need a public API at all.

10. Putting It All Together

  1. Create a new Next.js project: npx create-next-app@latest --api.
  2. Add Route Handlers inside the app/ directory (e.g., app/api/users/route.ts).
  3. Export HTTP methods (GET, POST, PUT, DELETE, etc.) in the same file.
  4. Use Web Standard APIs to interact with the Request object and return a Response.
  5. Build a public API if you need other clients to consume your data, or to proxy a backend service.
  6. Fetch your new API routes from the client (e.g., within a Client Component or with fetch('/api/...')).
  7. Or skip creating an API altogether if a Server Component can just fetch data.
  8. Add a shared “middleware” pattern (e.g., withAuth()) for auth or other repeated logic.
  9. Deploy to a Node.js-capable environment for server features, or export statically if you only need a static SPA.

Conclusion

Using the Next.js App Router and Route Handlers gives you a flexible, modern way to build APIs that embrace the Web Platform directly. You can:

  • Create a full public API to be shared by web, mobile, or third-party clients.
  • Proxy and customize calls to existing external services.
  • Implement a reusable “middleware” layer for authentication, logging, or any repeated logic.
  • Dynamically route requests using the [id] segment folder structure.

Frequently Asked Questions

What about Server Actions?

You can think of Server Actions like automatically generated POST API routes that can be called from the client.

They are designed for mutation operations, such as creating, updating, or deleting data. You call a Server Action like a normal JavaScript function, versus making an explicit fetch to a defined API route.

While there is still a network request happening, you don't need to manage it explicitly. The URL path is auto-generated and encrypted, so you can't manually access a route like /api/users in the browser.

If you plan to use Server Actions and expose a public API, we recommend moving the core logic to a Data Access Layer and calling the same logic from both the Server Action and the API route.

Can I use TypeScript with Route Handlers?

Yes, you can use TypeScript with Route Handlers. For example, defining the Request and Response types in your route file.

Learn more about TypeScript with Next.js.

What are the best practices for authentication?

Learn more in our authentication documentation.