Skip to content

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