Next.js as a backend: Server Actions, Route Handlers or a separate Node.js API?
Do you need Express or NestJS next to your Next.js app? What each Next.js server feature is for, when a separate Node.js API is worth it, and a decision checklist for startups and full-stack teams.
Vishal Solanki · Full-stack developer, Noida
"Do we need a separate backend?" is one of the first questions on almost every new project I take on. Usually the honest answer is: not yet. Next.js can do most of what a small product's backend needs. But there are clear signs that a separate Node.js API is worth the extra moving parts, and it's much cheaper to spot them early.
This post covers what each server-side feature in Next.js is actually for, where each one falls short, and when to add Express, NestJS or another service next to it.
The four server tools in Next.js
| Tool | What it's for | Who calls it |
|---|---|---|
| Server Components | Reading data and rendering it to HTML | Your own pages, during render |
| Server Actions | Mutations from your own UI: forms, buttons | Your own client code, over POST |
Route Handlers (route.ts) | Public HTTP endpoints that return anything: JSON, files, XML | Anyone: webhooks, mobile apps, partners, other services |
Proxy (proxy.ts) | Code that runs before a request: redirects, rewrites, headers | Every matching request |
In Next.js 16, Middleware was renamed to Proxy. It does the same job; the new name says more clearly what it's for.
Reading data: go straight to the source
In a Server Component, read from the database or the upstream service directly, ideally through one data access layer:
// app/orders/page.tsx
import { getOrdersFor } from "@/lib/data/orders";
import { getSession } from "@/lib/auth";
export default async function Orders() {
const session = await getSession();
const orders = await getOrdersFor(session.userId);
return <OrderList orders={orders} />;
}
A common mistake is calling your own Route Handler from a Server Component, like fetch("/api/orders"). That adds an HTTP round trip to your own server, and for pages pre-rendered at build time it fails outright, because there's no server running during the build. The Next.js docs call this out explicitly.
Mutations from your own UI: Server Actions
For forms and buttons in your app, Server Actions are the simplest option: no endpoint to name, progressive enhancement for forms, and you can revalidate the page in the same round trip.
Two things to know:
- They're public POST endpoints. Check the session, ownership and input inside every action. My Server Actions security checklist covers this step by step.
- They're queued. Actions from the same client run one after another. That's right for mutations, but it makes them a poor choice for fetching data.
Public endpoints: Route Handlers
Reach for a Route Handler when something other than your own UI needs to talk to your app:
- Webhooks: payment providers, CRMs and messaging tools post events to a URL you give them.
- Other clients: a mobile app, a partner integration or an internal tool that needs JSON.
- OAuth and payment callbacks, where a third party redirects the user back to you.
- Non-HTML responses: RSS, CSV exports, generated files.
// app/api/webhooks/payments/route.ts
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("x-signature");
if (!verifySignature(body, signature)) return new Response("Invalid signature", { status: 401 });
const event = JSON.parse(body);
await handlePaymentEvent(event); // idempotent: the provider may retry
return new Response(null, { status: 204 });
}
Treat every Route Handler like any public API: verify signatures or tokens, validate the payload, rate-limit, and never put a mutation behind GET.
Before the request: Proxy
Proxy is good at cheap, request-level decisions: redirecting old URLs, rewriting a path, adding security headers, sending signed-out users to the login page early. It is not a complete auth layer. The Next.js docs are explicit about this: still verify the session where the data is read or changed, because a misconfigured matcher can let a request through.
When a separate Node.js API is worth it
Next.js on its own starts to strain when one or more of these is true:
- Several clients share the same business logic. A web app, a mobile app and partner integrations all need the same rules. A standalone API, documented and versioned, keeps those rules in one place and lets each client move at its own pace.
- You have long-running or background work. Queues, scheduled jobs, large imports, PDF generation, anything that runs for minutes. Serverless functions have execution time limits, and request handlers aren't a good home for a job runner anyway.
- You need persistent connections. WebSockets and long-lived streams for chat, live tracking or collaborative editing need a server that keeps connections open, or a managed real-time service.
- The work is CPU-heavy. Image or video processing and heavy calculations shouldn't slow down page rendering.
- Teams or scaling are separate. A dedicated backend team, separate deploys, or very different traffic patterns between the website and the API.
- Compliance requires it. Some industries need sensitive processing in a specific network, region or audited service.
If none of these apply, a separate API mostly adds a second deploy, a second set of logs, CORS settings and an extra network hop.
The middle path: Next.js as the backend for your frontend
You don't have to choose all or nothing. A common, solid setup:
- Core services (Node.js with Express or NestJS, or anything else) own the business logic, the database and the background jobs.
- Next.js is the backend for the frontend. Server Components read from those services. Server Actions call them for mutations. Route Handlers expose only what outside clients need.
- The browser never talks to the core services directly. Tokens and service credentials stay on the Next.js server, and the user's session lives in an
httpOnlycookie.
This is roughly how I approach sensitive flows on real products: moving operations that used to run in the browser onto the server, behind secure cookies, so tokens and business rules are never exposed to client-side JavaScript.
Keep one data access layer either way
Whichever setup you choose, put data access behind one server-only module and call it from Server Components, Server Actions and Route Handlers alike:
src/
app/
orders/page.tsx ← reads via lib/data
orders/actions.ts ← "use server", thin, calls lib/data
api/webhooks/payments/route.ts ← public endpoint, calls lib/data
lib/
auth.ts ← getSession()
data/
orders.ts ← import "server-only"; queries + permission checks
If you later move to a separate API, only lib/data changes: it calls the API instead of the database. The pages, actions and handlers stay as they are.
A decision checklist
| Question | If yes |
|---|---|
| Is the only client your Next.js web app? | Start with Next.js alone |
| Will a mobile app or partners need the same logic soon? | Plan a standalone API, or at least a clean data layer you can move |
| Any jobs that run longer than a normal request? | Add a worker or queue service |
| Real-time features (chat, live updates)? | Add a WebSocket server or a managed real-time service |
| Heavy CPU work? | Separate service |
| Separate backend team or very different scaling needs? | Separate API |
| None of the above? | Next.js alone; revisit when one becomes true |
FAQ
Can Next.js replace Express?
For many web apps, yes. Server Components, Server Actions and Route Handlers cover reading, writing and public endpoints. Express or NestJS still wins for long-running jobs, persistent connections, multiple clients sharing one versioned API, and teams that want the backend deployed separately.
Should I build REST endpoints for my own frontend?
Usually not. Read data in Server Components and write with Server Actions. Build Route Handlers when a client other than your own UI needs them.
Is a Next.js backend production-ready?
Yes, with the same discipline as any backend: validation, authentication and authorisation on every entry point, rate limits on public ones, logging and monitoring, and keeping the framework patched.
Planning a new product, or untangling an existing one? I design and build both sides, from interface to API. Tell me what you're building.