Route Handlers let you build API endpoints directly inside your Next.js app, replacing the old API routes with a simpler, more flexible file-based approach. Here's a practical guide to using them effectively in full-stack projects.
If you're building a full-stack app with Next.js, Route Handlers are your primary way to create backend logic without spinning up a separate server. They live in the app directory, follow the same file conventions as pages, and give you full control over the HTTP layer. I've used them in production for everything from authentication to file uploads, and they hold up well under real traffic.
Why Route Handlers Matters (and When to Skip It)
Route Handlers aren't a silver bullet. They shine when you need server-side logic tightly coupled to your frontend—think form submissions, database queries, or proxying external APIs. But if you're building a public API for third-party consumers, or you need WebSocket support, you're better off with a dedicated backend like Express or Fastify.
The real win is the developer experience. You write your endpoint in the same file tree as your UI, share TypeScript types across the boundary, and deploy without configuring a separate service. That said, don't force it. If your project has complex background jobs or streaming needs, Route Handlers will fight you.
Getting Started with Route Handlers
Create a file under app/api/ and export a function named after the HTTP method. Here's a minimal example that returns JSON:
// app/api/hello/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ message: 'Hello from Route Handler' });
}
That's it. Hit /api/hello and you get {"message":"Hello from Route Handler"}. You can also handle POST, PUT, DELETE, PATCH, and OPTIONS by exporting those functions from the same file.
For dynamic routes, use folders with square brackets:
// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
export async function GET(
_request: Request,
{ params }: { params: { id: string } }
) {
return NextResponse.json({ userId: params.id });
}
Core Route Handlers Concepts Every Developer Should Know
1. Request and Response objects
Route Handlers use the standard Web Request and Response APIs. You parse the body with await request.json() and set headers on the response:
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json(
{ received: body },
{ status: 201, headers: { 'X-Custom-Header': 'value' } }
);
}
2. Dynamic segments and query params
Access URL parameters through params and query strings via request.nextUrl.searchParams:
export async function GET(request: Request) {
const url = new URL(request.url);
const search = url.searchParams.get('q');
return NextResponse.json({ query: search });
}
3. Caching and revalidation
By default, GET Route Handlers are not cached—they run on every request. But you can opt into caching with export const dynamic = 'force-static' or revalidate with export const revalidate = 3600. This is a game-changer for read-heavy endpoints that pull from a database.
Common Route Handlers Mistakes and How to Fix Them
Mistake 1: Forgetting to handle OPTIONS for CORS. If your frontend is on a different origin, you need to explicitly respond to preflight requests:
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
Mistake 2: Not validating input before hitting the database. Always validate request.json() payloads—use Zod or a manual check. I've seen too many production bugs from trusting client data.
Mistake 3: Using Route Handlers for server actions. If you're just mutating data from a form, use Server Actions instead. They handle progressive enhancement and revalidation automatically. Route Handlers are for API endpoints, not form submissions.
When Should You Use Route Handlers?
Use Route Handlers when you need a RESTful API for external clients like mobile apps, third-party integrations, or when you're fetching data from a frontend that isn't your own Next.js app. They're also the right choice for webhooks—GitHub, Stripe, or Slack callbacks need a stable, POST-only endpoint.
Skip them if you're only serving your own UI. Server Components and Server Actions cover most internal data needs with less boilerplate. And if you need long-lived connections, real-time updates, or heavy computation, pick a separate backend service.
Route Handlers in Production
1. Set up rate limiting. Route Handlers run on the same infrastructure as your pages, so they're exposed to the internet. Use a package like upstash-rate-limit or a simple in-memory counter for low-traffic apps.
2. Log everything. Wrap your handlers with a logging utility that captures method, path, status, and duration. It's painful to debug production issues without request traces.
3. Handle errors gracefully. Always wrap your logic in try/catch and return proper status codes—400 for bad input, 404 for missing resources, 500 for unexpected failures. Never leak stack traces to the client.
For a deeper dive into how I structure full-stack apps with Route Handlers, check out the projects on suhailroushan.com—I've got a few production examples that show the pattern in action.
The one thing I'd tell every developer: start with Route Handlers for your API layer, but keep them thin. Move business logic into separate service files so your handlers stay readable and testable. That separation has saved me countless hours debugging.