Building RESTful APIs with Next.js Route Handlers: Best Practices | SoniNow Blog

Limited TimeLearn More

apinext.jsrest apibackendroute handlers

Building RESTful APIs with Next.js Route Handlers: Best Practices

Published

2026-06-23

Read Time

4 mins

Building RESTful APIs with Next.js Route Handlers: Best Practices

Next.js Route Handlers (route.ts) provide a full backend layer within your App Router application. They run server-side, support all HTTP methods, and integrate seamlessly with middleware, authentication, and caching. Here is how to build production-grade REST APIs without spinning up a separate server.

Structuring Route Handlers for RESTful Design

Each route.ts file exports named functions for HTTP methods: GET, POST, PUT, PATCH, DELETE. Place them in folders that mirror your API structure under app/api/.

// app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'

export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const product = await db.product.findUnique({ where: { id } })

  if (!product) {
    return NextResponse.json(
      { error: 'Product not found' },
      { status: 404 }
    )
  }

  return NextResponse.json(product)
}

Use dynamic segments ([id]) for resource identifiers. Keep each handler focused on a single resource — this follows the REST principle of resources as nouns and methods as verbs.

Authentication and Authorization

Secure your handlers with middleware that runs before every API request. Validate JWTs or session tokens there rather than repeating auth logic in every handler.

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/admin')) {
    const token = request.cookies.get('session')?.value
    if (!token || !verifyToken(token)) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
  }
  return NextResponse.next()
}

For role-based access, decode the token and attach user info to request headers that downstream handlers can read. Avoid putting full user objects in middleware — pass a minimal identifier and let the handler fetch details if needed.

Error Handling and Consistent Responses

A unified error response format simplifies client-side error handling. Wrap your handler logic in a try-catch and return structured errors.

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const product = await db.product.create({ data: body })
    return NextResponse.json(product, { status: 201 })
  } catch (error) {
    console.error('Failed to create product:', error)

    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Validation failed', details: error.errors },
        { status: 422 }
      )
    }

    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

Use Zod or another schema validator at the handler boundary. Invalid input should never reach your database — catch it early and return a meaningful 422 response with field-level details.

Rate Limiting and Caching Strategy

Route Handlers benefit from Next.js built-in caching when you use NextResponse.json() with a Cache-Control header. For dynamic APIs, set caching headers to prevent over-fetching.

export async function GET() {
  const products = await db.product.findMany()
  return NextResponse.json(products, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
    },
  })
}

Implement rate limiting with an in-memory store or Redis. The Upstash Ratelimit library integrates cleanly with Next.js:

import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
})

export async function POST(request: NextRequest) {
  const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
  const { success } = await ratelimit.limit(ip)
  if (!success) {
    return NextResponse.json({ error: 'Too many requests' }, { status: 429 })
  }
  // ...
}

API Versioning and Documentation

Version your API through the URL path (/api/v1/products). Use route groups to avoid duplicating handler files. Document your API with a tool like Scalar or Stoplight — OpenAPI schemas generated from your Zod types keep documentation in sync with implementation.

Designing and deploying robust REST APIs involves more than writing route handlers. Database connection pooling, connection timeouts, error monitoring, and request logging all need attention. At SoniNow, we build full-stack Next.js applications with production-ready API layers that handle authentication, rate limiting, and observability out of the box.

Need a production API for your Next.js project? Reach out to SoniNow and let us architect a secure, scalable backend for your application.