Friday, February 28th 2025
Building APIs with Next.js
Posted byThis 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
- 2. Why (and when) to build APIs with Next.js
- 3. Creating an API with Route Handlers
- 4. Working with Web APIs
- 5. Dynamic routes
- 6. Using Next.js as a proxy or forwarding layer
- 7. Building shared “middleware” logic
- 8. Deployment and “SPA Mode” considerations
- 9. When to skip creating an API endpoint
- 10. Putting It All Together
- Conclusion
- Frequently Asked Questions
1. Getting started
1.1 Create a Next.js app
If you’re starting fresh, you can create a new Next.js project using:
npx create-next-app@latest --api
Note: The
--api
flag automatically includes an exampleroute.ts
in your new project’sapp/
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 placeroute.ts
orroute.js
files anywhere inside theapp/
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
-
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.
-
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.
-
Webhooks and Integrations
- If you receive external callbacks or webhooks (e.g., from Stripe, GitHub, Twilio), you can handle them with Route Handlers.
-
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.
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
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
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.
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:
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:
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:
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:
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:
// (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
- Create a new Next.js project:
npx create-next-app@latest --api
. - Add Route Handlers inside the
app/
directory (e.g.,app/api/users/route.ts
). - Export HTTP methods (
GET
,POST
,PUT
,DELETE
, etc.) in the same file. - Use Web Standard APIs to interact with the
Request
object and return aResponse
. - Build a public API if you need other clients to consume your data, or to proxy a backend service.
- Fetch your new API routes from the client (e.g., within a Client Component or with
fetch('/api/...')
). - Or skip creating an API altogether if a Server Component can just fetch data.
- Add a shared “middleware” pattern (e.g.,
withAuth()
) for auth or other repeated logic. - 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.