Module F-7·14 min read

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.

Written by Jatin Jain Saraf · Architect of SupraScan

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 & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.