Engineering Notes
Home/Backend/Frameworks/Next.js as a Backend
Backend · Frameworks

Next.js as a Backend

Next.js route handlers let one codebase serve both pages and APIs. Decide when that's the right call with the decision helper, choose between Edge and Node runtimes, and read bodies and query params correctly.

⚙️ Backend● Intermediate⏱ 4 min read▶ 1 interactive🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Route handlers as backend endpoints
  • When Next.js-as-backend fits (and when it doesn't)
  • Edge runtime vs Node.js runtime
  • Reading request bodies and query params

1Route Handlers: Next.js's Answer to a Backend Endpoint

Next.js's App Router lets a route.ts file inside app/ define an HTTP endpoint directly, alongside the same-framework pages that render UI (already covered in frontend/ecosystem/01-nextjs_fundamentals/ and 02-nextjs_routing/):

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

export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
  const user = await db.users.findById(params.id);
  if (!user) {
    return NextResponse.json({ error: { code: "NOT_FOUND" } }, { status: 404 });
  }
  return NextResponse.json(user);
}

export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
  await db.users.delete(params.id);
  return new NextResponse(null, { status: 204 });
}

Each exported function name (GET, POST, PUT, DELETE, PATCH) maps directly to that HTTP method for the route — this mirrors app.get()/app.post() from 01-express_fundamentals/, just expressed as named exports tied to a filesystem path instead of imperative registration calls. The [id] folder segment is Next.js's dynamic-route syntax (the same mechanism 02-nextjs_routing/ covers for pages), and it populates params.id here exactly the way :id populates req.params.id in Express.

2When Next.js-as-Backend Is the Right Call

Using Next.js's own Route Handlers as your entire backend makes sense when:

  • The backend's job is primarily serving the same app's own frontend — form submissions, simple CRUD backing the UI, auth callbacks — rather than being a general-purpose API consumed by many unrelated clients.
  • You want one deployable unit (one repo, one build, one deploy) instead of coordinating a separate frontend and backend service.
  • The backend logic is light enough that NestJS's or Express's dedicated backend structure (modules, DI, a real service layer — 02-nestjs_fundamentals/) would be more ceremony than the app needs.

It's a weaker fit when the backend needs to be a genuinely independent, heavily-used API serving multiple different frontends/clients, needs its own independent scaling and deploy lifecycle separate from the UI, or needs the structured module/DI system a framework like NestJS provides for a large team. Many real systems pair Next.js (frontend + light backend-for-frontend routes) with a separate backend service for the substantial API — that's a legitimate architecture, not a compromise.

3Edge Runtime vs. Node.js Runtime

Every Route Handler runs in one of two runtimes, chosen per-route:

ts
export const runtime = "edge"; // or "nodejs" (the default)
Node.js runtime (default)Edge runtime
Available APIsFull Node.js API surface (fs, most npm packages, full Buffer)A restricted, Web-standard-API-only subset (fetch, Request/Response, no fs, no arbitrary native Node modules)
Where it runsA traditional server process/containerDistributed at the network edge, closer to the requesting user
Cold startSlowerNear-instant
Best forDatabase access via most ORMs/drivers, anything needing full Node APIsSimple, fast, latency-sensitive logic (auth checks, redirects, lightweight transforms) that doesn't need Node-specific APIs

Choosing edge for a route that needs a Node-only database driver will fail at build or runtime, not silently degrade — the restriction is real, not just a performance suggestion. Most routes doing real database work stay on the default nodejs runtime.

A route.ts file exports HTTP method handlers that run either on the Node.js runtime with full Node APIs or on the Edge runtime with Web APIs only
A route.ts file exports HTTP method handlers that run either on the Node.js runtime with full Node APIs or on the Edge runtime with Web APIs only

4Reading the Request Body and Query Params

ts
export async function POST(req: NextRequest) {
  const body = await req.json(); // NextRequest extends the Web Request API -- body reading is async
  const url = new URL(req.url);
  const limit = url.searchParams.get("limit"); // query params via the standard URL API, not req.query
  return NextResponse.json({ received: body, limit });
}

Note the contrast with Express's req.body (already parsed and synchronously available once express.json() middleware has run, per 01-express_fundamentals/ section 5): NextRequest is built on the standard Web Request API, so the body is read via an explicit await req.json() call, and query params come from parsing req.url with the standard URL/URLSearchParams API rather than a framework-populated req.query object.

⚠️ Common Pitfalls

  1. Forgetting that route handler exports are named by HTTP method. A function named handler instead of GET/POST/etc. is simply never called — Next.js looks specifically for those exact export names.
  2. Choosing edge runtime for a route that needs a Node-only dependency (most traditional SQL drivers, fs-based logic) — fails outright rather than falling back.
  3. Treating Next.js-as-backend as automatically the right architecture for a large, independently-scaled API — it's a good fit for backend-for-frontend needs, not a universal replacement for a dedicated backend service (section 2).
  4. Forgetting await on req.json() — since the Web Request body API is asynchronous by design, unlike Express's already-parsed req.body.

📋 Quick Reference

text
app/api/.../route.ts
  export async function GET/POST/PUT/DELETE/PATCH(req, { params })

export const runtime = "nodejs" | "edge"   -- per-route runtime choice
  nodejs -- full Node API surface, default, needed for most DB drivers
  edge   -- Web-standard APIs only, near-instant cold start, no fs/native Node modules

await req.json()          -- read the body (async, Web Request API)
new URL(req.url).searchParams.get("x")  -- query params (no req.query)
NextResponse.json(obj, { status })       -- send a JSON response

🧠 Key takeaways

  • app/api/**/route.ts exports GET, POST… functions using Web Request/Response.
  • Great as a BFF for its own frontend; weaker for public multi-client APIs, long jobs and sockets.
  • Edge runtime: fast cold starts, Web APIs only, limited Node APIs; Node runtime: full Node, DB drivers.
  • Keep business logic in framework-free modules so it can move to a separate service later.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1How can Next.js act as a backend?core▾

Through Route Handlers (app/api/.../route.ts) for HTTP endpoints, Server Actions for form/mutation handling, server components that query data directly, and middleware at the edge. It's ideal for a backend-for-frontend.

Follow-ups interviewers ask
  • Pages Router equivalent? pages/api API routes.
Q2When is Next.js a good backend and when isn't it?core▾

Good for BFF needs: data aggregation for the UI, auth flows, simple CRUD. Less suitable for long-running jobs, WebSocket-heavy apps, complex domain backends shared by many clients, or heavy background processing; use a dedicated service there.

Follow-ups interviewers ask
  • Why are long jobs a problem on serverless? Execution time limits and no persistent processes.
Q3What are the runtime options and their trade-offs?senior▾

Node.js runtime: full Node APIs and libraries. Edge runtime: fast startup, runs close to users, but limited APIs (no native modules, limited Node built-ins). Choose per route.

Follow-ups interviewers ask
  • What runs in middleware? Edge runtime by default.
Q4How do you secure Route Handlers and Server Actions?core▾

Authenticate and authorize inside every handler/action (they're public endpoints), validate input with schemas, protect against CSRF (Server Actions check Origin), and never trust hidden form fields.

Follow-ups interviewers ask
  • Are Server Actions private functions? No, they're callable HTTP endpoints.
Q5How do you handle database connections in serverless Next.js?core▾

Use connection poolers (PgBouncer, RDS Proxy, Prisma Accelerate/Neon serverless drivers), reuse clients across invocations via module scope, and avoid opening a new pool per request.

Follow-ups interviewers ask
  • Why is it an issue? Many concurrent function instances can exhaust DB connections.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.

✅ Check yourself

Answer the questions — feedback is instant.

1. Which runtime can use a native PostgreSQL driver with TCP sockets most easily?
Edge lacks most Node APIs.
2. A mobile app and three partner integrations need a versioned API. Best home?
Independent lifecycle.
3. How do you read JSON in a route handler?
Web Request API.
4. Why keep business logic out of route.ts files?
Portability.