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.
🎯 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
📚 Before you start
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/):
// 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:
export const runtime = "edge"; // or "nodejs" (the default)| Node.js runtime (default) | Edge runtime | |
|---|---|---|
| Available APIs | Full 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 runs | A traditional server process/container | Distributed at the network edge, closer to the requesting user |
| Cold start | Slower | Near-instant |
| Best for | Database access via most ORMs/drivers, anything needing full Node APIs | Simple, 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.
4Reading the Request Body and Query Params
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
- Forgetting that route handler exports are named by HTTP method. A function named
handlerinstead ofGET/POST/etc. is simply never called — Next.js looks specifically for those exact export names. - Choosing
edgeruntime for a route that needs a Node-only dependency (most traditional SQL drivers,fs-based logic) — fails outright rather than falling back. - 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).
- Forgetting
awaitonreq.json()— since the WebRequestbody API is asynchronous by design, unlike Express's already-parsedreq.body.
📋 Quick Reference
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.tsexportsGET,POST… functions using WebRequest/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
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.
- Pages Router equivalent?
pages/apiAPI 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.
- 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.
- 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.
- 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.
- 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.