Route Handlers let you build API endpoints right alongside your React components in Next.js’s App Router.
Here’s a GET request handler for a simple /api/users endpoint:
// app/api/users/route.js
import { NextResponse } from 'next/server';
export async function GET(request) {
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
return NextResponse.json(users);
}
And here’s a POST request handler to add a new user:
// app/api/users/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
const newUser = await request.json();
// In a real app, you'd save this to a database
console.log('Received new user:', newUser);
return NextResponse.json({ message: 'User created', user: newUser }, { status: 201 });
}
When you deploy this, Next.js automatically creates the /api/users route. A GET request to /api/users will trigger the GET function, returning the list of users. A POST request to the same URL with a JSON body will trigger the POST function, logging the new user and responding with a 201 Created status.
The core problem Route Handlers solve is bringing API development into the same project structure as your frontend, eliminating the need for a separate backend service for many common use cases. They leverage the built-in routing of the App Router. You define a route.js or route.ts file within your app directory, and Next.js maps that file’s path to an API endpoint.
Internally, Route Handlers are server-side functions. When a request hits a path that maps to a route.js file, Next.js executes the corresponding HTTP method function (e.g., GET, POST, PUT, DELETE). You receive the incoming Request object and return a Response object, often using NextResponse for convenience.
The exact levers you control are the HTTP methods you export (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS). Each function receives the native Web Request object and should return a native Web Response or a NextResponse instance. You can access request details like query parameters, search parameters, headers, and the request body. For JSON bodies, request.json() is your go-to. For form data, request.formData().
// app/api/items/[id]/route.js
import { NextResponse } from 'next/server';
export async function GET(request, { params }) {
const itemId = params.id; // e.g., if request is /api/items/123, itemId will be "123"
const item = { id: itemId, name: `Item ${itemId}` };
return NextResponse.json(item);
}
export async function PUT(request, { params }) {
const itemId = params.id;
const updatedData = await request.json();
console.log(`Updating item ${itemId} with:`, updatedData);
return NextResponse.json({ message: `Item ${itemId} updated`, data: updatedData });
}
This example shows dynamic routes. If you request /api/items/456, params.id will be 456. The PUT handler would then expect a JSON body to update that specific item.
The most surprising thing about Route Handlers is that they don’t just run on the server; they can also be deployed as Edge Functions, giving you incredibly low latency for API requests without the overhead of a traditional server. This means you can have lightning-fast APIs geographically closer to your users by simply changing your next.config.js or using specific deployment configurations.
The next concept you’ll likely explore is handling different content types beyond JSON, such as form data or file uploads, within your Route Handlers.