Next API Routes¶
Route handlers are Next.js's answer to building backend APIs without spinning up a separate server. They live in your app directory , share the same TypeScript types as your frontend , and deploy wherever your Next.js app deploys
One codebase. One deployment. One set of environment variables. Route handlers make the traditional "React frontend + Express backend" architecture feel like overengineered nonsense — until you hit the limits of what route handlers can do
basic route handler¶
A route.ts file in the app/api/ directory creates an API endpoint
// src/app/api/hello/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ message: 'Hello from Next.js API' })
}
export async function POST(request: Request) {
const body = await request.json()
return NextResponse.json({ received: body }, { status: 201 })
}
The file exports named functions for each HTTP method: GET , POST , PUT , PATCH , DELETE , HEAD , OPTIONS
Route mapping:
src/app/api/hello/route.ts -> /api/hello
src/app/api/users/route.ts -> /api/users
src/app/api/users/[id]/route.ts -> /api/users/123
src/app/api/products/[...slug]/route.ts -> /api/products/a/b/c
request and response typing¶
NextRequest extends the standard Request with helper methods
// src/app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
// Query parameters
const { searchParams } = new URL(request.url)
const page = searchParams.get('page') || '1'
const limit = searchParams.get('limit') || '10'
// Cookies
const token = request.cookies.get('session-token')
// Headers
const userAgent = request.headers.get('user-agent')
// Geolocation (only on Vercel Edge)
const country = request.geo?.country
return NextResponse.json({
page,
limit,
country
})
}
NextResponse extends Response with cookie manipulation
// src/app/api/auth/login/route.ts
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const body = await request.json()
// ... validate credentials ...
const response = NextResponse.json({ success: true })
// Set secure cookie
response.cookies.set('session-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 1 week
})
return response
}
dynamic route handlers¶
Route handlers support the same dynamic segments as pages
// src/app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const product = await prisma.product.findUnique({
where: { id }
})
if (!product) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
)
}
return NextResponse.json(product)
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const body = await request.json()
const updated = await prisma.product.update({
where: { id },
data: body
})
return NextResponse.json(updated)
}
middleware for API routes¶
You can protect API routes using middleware
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Only protect API routes
if (request.nextUrl.pathname.startsWith('/api/admin')) {
const token = request.cookies.get('admin-token')
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*'
}
Middleware runs before the route handler. For API-specific middleware (rate limiting , request logging , CORS) , handle it in the middleware file , not in every route handler
CORS configuration¶
Browser-based API calls need CORS headers. Route handlers don't add them automatically
// src/app/api/data/route.ts
import { NextResponse } from 'next/server'
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://your-app.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
export async function OPTIONS() {
return NextResponse.json({}, { headers: corsHeaders })
}
export async function GET() {
return NextResponse.json(
{ data: 'your data' },
{ headers: corsHeaders }
)
}
The OPTIONS handler is required for CORS preflight requests. Forgetting it means your frontend calls fail with cryptic network errors
edge vs node runtime¶
Route handlers can run in two runtimes: Edge or Node
// Edge runtime — fast boot , limited APIs
// src/app/api/edge-example/route.ts
export const runtime = 'edge'
export async function GET() {
return new Response('Fast but limited', {
headers: { 'x-edge': 'true' }
})
}
// Node runtime — full access , slower boot
// src/app/api/node-example/route.ts
export const runtime = 'nodejs'
import fs from 'fs'
import path from 'path'
export async function GET() {
const data = fs.readFileSync(path.join(process.cwd(), 'data.json'), 'utf-8')
return new Response(data)
}
Edge runtime can't: * Access filesystem (fs , path) * Use native Node.js modules (crypto with native bindings , bcrypt) * Run long requests (30s timeout on Vercel) * Use Prisma or other ORMs with native drivers
Node runtime can: * Everything Node.js can do * But slower cold starts on serverless platforms
Choose Edge for low-latency , high-throughput APIs. Choose Node when you need filesystem access , native modules , or database connections
prerequisites¶
next_07_server_actions.md — you should understand Server Actions and when to use route handlers instead
next → next_09_auth.md