Next.js API Routes Explained

Your Next.js app can serve its own API — no separate Express server, no CORS, one deployment. This guide explains Route Handlers in the App Router with working examples, the database singleton pattern, auth, caching gotchas, and the limits where a standalone API still wins.

Written by Projectech7 min readPublished
For B.E./B.Tech Computer Science and IT students building Next.js apps who are running a separate Express backend and want to understand the built-in API option Topics: Next.js, React, TypeScript, APIs
Illustration of Next.js API routes showing files in an app api directory mapping to HTTP endpoints served by the same application.
Illustration generated for this guide.
In this guide

You built a Next.js frontend and a separate Express backend. Two repos (or two folders), two dev servers, CORS configuration to make them talk, and two deployments to manage. For many projects, that second server is unnecessary weight — Next.js can serve your API itself.

API Routes (and their successor, Route Handlers) let your Next.js app respond to HTTP requests directly: same project, same deployment, no CORS. This guide explains how they work, when they fit, and where their limits are.

The mental model

A file in a special directory becomes an HTTP endpoint:

app/api/users/route.ts      ->  GET/POST https://yourapp.com/api/users
app/api/users/[id]/route.ts ->  GET/PUT/DELETE https://yourapp.com/api/users/42

The filesystem is the router — the same convention Next.js uses for pages. Dynamic segments in square brackets become URL parameters. This colocation is the appeal: your frontend and its backend live in one codebase and deploy as one unit.

Note: The older Pages Router used pages/api/ files with (req, res) handlers. The App Router (Next.js 13+) uses app/api/**/route.ts with Web-standard Request/Response. This guide covers the App Router — use it for new projects.

Your first Route Handler

// app/api/users/route.ts
import { NextResponse } from 'next/server';

// GET /api/users
export async function GET() {
  const users = await db.user.findMany();
  return NextResponse.json(users);
}

// POST /api/users
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();

  if (!body.email || !body.name) {
    return NextResponse.json(
      { error: 'name and email are required' },
      { status: 400 }
    );
  }

  const user = await db.user.create({ data: body });
  return NextResponse.json(user, { status: 201 });
}

Export a function named after the HTTP method (GET, POST, PUT, PATCH, DELETE); Next.js wires it up. NextResponse.json() builds the JSON response with status codes. The handler receives a standard web Requestrequest.json(), request.headers, request.url all work as on any platform.

Dynamic routes and query params

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({
    where: { id: Number(params.id) },
  });

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

URL segments arrive via the second argument's params; query strings via request.nextUrl.searchParams.get('q'). Validate params.id before using it — it is a string from the URL, and Number("abc") is NaN, not an error.

When API Routes are the right call

  • Your frontend needs a backend for frontend (BFF). Form submissions, data fetching for your own pages, small CRUD — the classic student project backend.
  • You want one deployment. Vercel, or a single VPS running next start, serves pages and API together. No CORS, no second server to babysit.
  • Server-side secrets. API keys and database credentials stay in Route Handlers (server-only) instead of leaking into browser bundles.
  • Prototyping speed. New endpoint = new file. No separate backend repo to scaffold.

Where they hit limits

Concern Reality
Long-running work Serverless deployments cap execution time (often 10–60s). ML inference, video processing, big reports need a separate worker or message queue.
WebSockets Route Handlers are request/response — no persistent connections. Real-time needs a separate server (see the WebSockets guide).
Heavy traffic One Next.js server handling pages + API + SSR can become the bottleneck; a dedicated API scales independently.
Non-JS services If your ML model serves from Python or your team speaks Go, a separate API service is cleaner.

The honest rule: start with Route Handlers; extract a standalone API when a specific limit bites. Most student projects never hit the limits.

Database access pattern

Connect once and reuse the client across requests — do not open a new database connection per request:

// lib/db.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const db = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = db;
}

In development, Next.js hot-reloads modules constantly; without the global cache you leak a database connection per reload until the database refuses new ones. This singleton pattern is the standard fix (shown here with Prisma; the idea applies to any client).

Authentication in Route Handlers

Route Handlers read cookies and headers like any server code:

Middleware (middleware.ts) can protect whole route groups — redirect unauthenticated users before the handler runs — keeping auth logic out of every file.

Caching and rendering interplay

Route Handlers support the same caching primitives as the rest of Next.js:

  • export const dynamic = 'force-dynamic' — always run fresh (for personalized data).
  • revalidate — cache the response and refresh on an interval (for semi-static data like a product list).

Default caching in App Router surprises beginners: a GET handler can be statically cached at build time. If your endpoint returns per-user data and shows stale results, force-dynamic is usually the fix.

Route Handlers vs Server Actions

Next.js offers two server-side primitives and beginners mix them up. The rule of thumb:

Route Handlers Server Actions
What HTTP endpoints (app/api/**/route.ts) Async functions called directly from components
Use for Public APIs, webhooks, third-party callbacks, anything needing raw HTTP control Form submissions and mutations inside your own app
Called by Any HTTP client Your React components (via action or useTransition)
Response Full control (status codes, headers, streaming) Return values to the component

A contact form in your app? Server Action — no endpoint file, no fetch boilerplate, progressive enhancement built in. A Stripe webhook or a mobile app calling your backend? Route Handler — you need the HTTP semantics. They coexist fine: many apps use Server Actions for their own UI and Route Handlers for everything external.

Common mistakes

  • Putting secrets in client components. Anything imported by a client component ships to the browser. Database clients and API keys live in Route Handlers and server components only.
  • New DB connection per request. Use the singleton pattern above.
  • No input validation. Route Handlers are a public HTTP surface — validate bodies and params (Zod pairs well with TypeScript) instead of trusting them.
  • Long tasks in handlers. A 5-minute report generation will hit execution timeouts on serverless. Queue it, return 202, let the client poll or use WebSockets for completion.
  • Forgetting force-dynamic. Stale personalized data from build-time caching is a confusing bug the first time.
  • CORS confusion. Same-origin requests need no CORS. You only configure CORS when other origins call your API — which, with a colocated frontend, is nobody.

Quick checklist

  • Routes organized under app/api/ mirroring your resource model
  • Input validation on every handler that accepts data
  • Database client as a singleton, not per-request
  • Auth verified server-side on protected routes (middleware for groups)
  • Caching behavior deliberate (force-dynamic where data is personal)
  • Long-running work moved to queues/workers, not handlers

Where to go from here

More project guides

More in Web Development