Building GET/POST/PUT/DELETE handlers with NextRequest and NextResponse, the new proxy.js convention, userAgent() for device detection, and when to use a Route Handler vs a Server Action.
F-7 — Route Handlers and the Backend-for-Frontend Pattern
Who this is for: Developers who understand Server Components and data fetching from F-3 and F-4, and need to build API endpoints within a Next.js application — for third-party webhooks, mobile clients, public APIs, or proxying external services. This module also clarifies the often-confused question of when to use a Route Handler versus a Server Action.
When You Actually Need a Route Handler
Most data operations in a modern Next.js application don't need a Route Handler. Server Components fetch data directly. Server Actions handle mutations. Between those two primitives, the majority of the client-server communication in your app can happen without you ever writing an HTTP endpoint.
But some situations genuinely require one:
Third-party webhooks. Stripe, GitHub, Shopify, and every other service that sends webhook events needs a publicly accessible HTTP endpoint to POST to. Server Actions can't receive requests from external systems. Route Handlers can.
Public APIs for non-browser clients. If you have a mobile app, a CLI tool, or any consumer that isn't your Next.js frontend, you need a real HTTP API. Route Handlers are that API.
OAuth and auth callbacks. Authentication flows that redirect back to your app with a code or token need a dedicated endpoint. NextAuth handles this internally with Route Handlers under the hood.
Response streaming. Server-Sent Events (SSE), large file downloads, or any response where you need control over the HTTP response body as a stream.
Custom headers or response formats. If a client needs a response in a specific format (CSV export, XML feed, binary data), Route Handlers give you direct control over the Response object.
For everything else — mutations triggered by user interaction in your own frontend, data loading for your own pages — Server Actions and Server Components are the right tools.
The Basics
A route.ts file in any app/ directory folder creates an HTTP endpoint. Export a function named after the HTTP verb:
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const products = await db.products.findMany({
orderBy: { createdAt: 'desc' },
take: 50,
});
return NextResponse.json(products);
}
export async function POST(request: NextRequest) {
const body = await request.json();
// Basic validation
if (!body.name || !body.price) {
return NextResponse.json(
{ error: 'name and price are required' },
{ status: 400 }
);
}
const product = await db.products.create({ data: body });
return NextResponse.json(product, { status: 201 });
}
Any HTTP verb not exported returns 405 Method Not Allowed automatically. Supported verbs: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.
Dynamic Route Handlers
Route Handlers work with the same dynamic segment syntax as pages:
// app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface RouteContext {
params: Promise<{ id: string }>;
}
export async function GET(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
const product = await db.products.findUnique({ where: { id } });
if (!product) {
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
}
return NextResponse.json(product);
}
export async function PATCH(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
const body = await request.json();
const product = await db.products.update({
where: { id },
data: body,
});
return NextResponse.json(product);
}
export async function DELETE(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
await db.products.delete({ where: { id } });
return new Response(null, { status: 204 });
}
params in Route Handlers is also a Promise in Next.js 15 — await it, just like in pages.
Reading the Request
NextRequest extends the standard Web API Request with Next.js-specific utilities:
export async function GET(request: NextRequest) {
// URL and query string
const url = request.nextUrl;
const category = url.searchParams.get('category');
const page = parseInt(url.searchParams.get('page') ?? '1');
// Headers
const authHeader = request.headers.get('authorization');
const contentType = request.headers.get('content-type');
// Cookies
const sessionCookie = request.cookies.get('session')?.value;
// Body (POST/PATCH/PUT)
const json = await request.json(); // parse as JSON
const text = await request.text(); // parse as text
const form = await request.formData(); // parse as FormData
const buffer = await request.arrayBuffer(); // parse as binary
}
Building Responses
Use NextResponse for most cases, or the standard Web API Response when you need more control:
// JSON response
return NextResponse.json({ message: 'ok' }, { status: 200 });
// Custom headers
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, max-age=60, stale-while-revalidate=300',
'X-Custom-Header': 'value',
},
});
// No-body response (204)
return new Response(null, { status: 204 });
// Text response
return new Response('Hello, world', {
headers: { 'Content-Type': 'text/plain' },
});
// Binary/file response
const fileBuffer = await readFile('./report.pdf');
return new Response(fileBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="report.pdf"',
},
});
// Redirect
return NextResponse.redirect(new URL('/login', request.url));
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion