Next.js has a backend, and it comes in two shapes. Route Handlers are HTTP endpoints anyone can call. Server Actions are functions your own components invoke, which happen to execute on the server. Use handlers for public APIs and webhooks, actions for mutations from your own UI.
The question of whether Next.js can be your backend gets a confused answer because two different mechanisms overlap, and because the more consequential question — where the code physically runs, and with what constraints — is rarely part of the discussion at all. The runtime you deploy to decides whether a database connection pool or a two-minute job is even possible.
Table of contents
- Route Handlers
- Server Actions
- Choosing between them
- Node runtime versus edge runtime
- Where the Next.js backend stops being enough
- Practical rules that avoid the common mistakes
- How this fits the rest of the stack
- FAQ
Route Handlers
A Route Handler is a file that exports functions named after HTTP methods. It uses the Web Request and Response APIs rather than a framework-specific abstraction.
// app/api/orders/route.ts
import type { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const status = request.nextUrl.searchParams.get('status')
const orders = await db.order.findMany({ where: { status } })
return Response.json(orders)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const order = await db.order.create({ data: body })
return Response.json(order, { status: 201 })
}
These are ordinary HTTP endpoints. They have a URL, they accept any method you export, and anything on the internet can call them. That is the defining property, and it determines when they are the right choice.
Use a Route Handler when the caller is not your own React components:
- A webhook from a payment provider, a mail service, or a repository host.
- A public or partner API.
- A mobile application talking to the same backend.
- A cron job or a scheduled task hitting an endpoint.
- Anything needing a method other than POST, or streaming, or custom headers and status codes.
Because they are public, they need their own authentication, input validation, and rate limiting. Nothing about being inside a Next.js app makes an endpoint private.
Server Actions
A Server Action is an async function marked with a directive, which you call directly from a component. Next.js handles the network round trip.
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
if (!title || title.length < 3) {
return { error: 'Title must be at least 3 characters' }
}
await db.post.create({ data: { title } })
revalidatePath('/posts')
return { success: true }
}
// app/posts/new/page.tsx
import { createPost } from '../../actions'
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
)
}
There is no fetch call, no endpoint URL, and no manual serialisation. The form works before JavaScript loads, because it is a real form posting to the server, which is a genuine advantage over a client-side handler.
Two constraints worth knowing. Server Actions are invoked over POST only, so they are not a substitute for a REST API. And although they have a generated endpoint underneath, that endpoint is an implementation detail rather than a stable public interface — do not build an external integration against it.
Being callable from your own UI does not make them trusted. The generated endpoint is reachable, so every action must authenticate and validate its input exactly as a public endpoint would. Treating an action as internal and skipping the authorisation check is the main security mistake in this area.
Choosing between them
A short decision rule that covers nearly every case.
- Your own form or component performing a mutation, and you want the result reflected in the UI: Server Action.
- Anything outside your application calling in: Route Handler.
- You need GET, PUT, DELETE, custom status codes, or streaming: Route Handler.
- You want the form to work without JavaScript: Server Action.
- You need progressive enhancement plus optimistic UI: Server Action with useOptimistic.
- Data fetching for a page: neither — fetch directly in the Server Component.
That last point deserves emphasis because it is the most common unnecessary pattern. A Server Component can query the database directly. Writing a Route Handler and then fetching it from your own Server Component adds an HTTP round trip from the server to itself, along with serialisation on both ends, to accomplish something the component could have done inline.
// No API layer needed for reads
export default async function PostsPage() {
const posts = await db.post.findMany()
return <PostList posts={posts} />
}
Node runtime versus edge runtime
This is the part usually omitted, and it decides what your backend can actually do.
Next.js server code can target two runtimes. The Node runtime is full Node.js with the standard library, native modules, TCP sockets, and no hard execution limit. The edge runtime is a restricted V8 environment distributed close to users, with fast cold starts, a small memory ceiling, a short execution limit, and no Node APIs.
What that rules out on edge:
- Most database drivers, which open raw TCP connections. Edge-compatible access means an HTTP-based driver or a proxy in front of the database.
- Persistent connection pooling. Edge functions are short-lived and distributed, so a pool per instance is either impossible or actively harmful to a database with a connection limit.
- Anything CPU-heavy or long-running — image processing, report generation, large uploads.
- Native modules and most of the Node standard library.
You choose per route:
export const runtime = 'nodejs' // or 'edge'
The practical guidance: edge for lightweight work that benefits from being near the user, such as redirects, header rewriting, geolocation, and authentication checks against a token. Node for anything touching a conventional database, doing real work, or using an ecosystem library.
There is a further distinction on top of runtime, which is whether the deployment is serverless or a long-running server. Serverless functions cold-start and are ephemeral, which is why connection pooling to a traditional database is a recurring problem in that model — each instance opens its own connections and a burst of traffic exhausts the database’s limit. A long-running Node server holds one pool for its lifetime and does not have the problem. Which one you get depends on where you deploy, and it is worth knowing before you design the data layer.
Where the Next.js backend stops being enough
It is a genuinely capable backend for a large class of applications. There are specific points at which reaching for a separate service is the right call.
- Long-running work. Report generation, video processing, or a large import does not belong in a request. It belongs in a queue and a worker, which is a separate process regardless of framework.
- Scheduled jobs. A cron endpoint triggered by an external scheduler works and is fragile. A real worker with a scheduler is sturdier.
- Persistent connections. WebSockets and long-lived streams need a long-running server, which sits awkwardly in a serverless deployment model.
- Multiple consumers. Once a mobile app and a partner integration both need the same API, that API is a product of its own and coupling its release cycle to your frontend deployments becomes an obstacle.
- Different scaling shapes. If the API takes ten times the traffic the pages do, scaling them together means paying for frontend capacity you do not need.
None of these mean starting over. The usual path is a Next.js application that keeps its UI-facing mutations as Server Actions and moves the heavy or shared work into a separate service alongside it — a Node or Python service, a worker, and a database, deployed together.
Next.js is a first-class runtime on RunxBuild, deployed from a repository with build logs, environment variables, a live route, and rollback to a previous deploy. A worker or an API service sits beside it on the same platform with a managed Postgres or MySQL on the private network, which is the shape most of these applications end up in.
Practical rules that avoid the common mistakes
- Validate input in every Server Action and Route Handler. A schema validator at the boundary, not manual checks scattered through the body.
- Authenticate inside the action, not in the component that calls it. The generated endpoint is reachable independently of your UI.
- Never import a module holding secrets into a Client Component. Keep server-only code in files marked server-only so the mistake is a build error rather than a leak.
- Use revalidatePath or revalidateTag after a mutation so the cache reflects the change. Forgetting this is why a form appears to do nothing.
- Return errors as values rather than throwing across the boundary. Thrown errors are sanitised in production and the useful message disappears.
- Pick the runtime deliberately per route rather than accepting a default that may not suit a database-backed handler.
The security point is worth repeating because it is the one with consequences. Server Actions feel like internal function calls and are not. Every one of them is an endpoint, and it must check who is calling before it does anything.
How this fits the rest of the stack
Next.js is a real backend for most applications, provided you use Route Handlers for anything external, Server Actions for your own mutations, and choose the runtime with the data layer in mind. It stops being sufficient at long-running work, persistent connections, and APIs with several consumers — and the answer then is a service beside it rather than a rewrite. Next.js is a supported runtime on RunxBuild with build logs, a live route, environment variables, and rollback, and the RunxBuild hosting calculator shows what the app, a worker, and a managed database come to together.
Useful related references:
- The Next.js Framework: What It Adds and What It Costs
- Next.js Middleware: What Belongs In It and What Will Bite You
- Next.js img vs Image: When the Component Is Worth It
- Services on RunxBuild
FAQ
Should I use Server Actions or Route Handlers?
Server Actions for mutations invoked by your own components, because they need no endpoint, no fetch call, and work before JavaScript loads. Route Handlers for anything external — webhooks, public APIs, mobile clients, cron triggers — and for anything needing a method other than POST, custom status codes, or streaming.
Can Next.js replace a separate backend?
For most applications, yes. It stops being enough when you need long-running jobs, persistent WebSocket connections, an API consumed by several clients on its own release cycle, or independent scaling for the API and the pages. The usual answer then is a separate service alongside the Next.js app, not a rewrite.
What is the difference between the Node and edge runtimes?
Node is full Node.js with the standard library, TCP sockets, and no hard execution limit. Edge is a restricted V8 environment near the user with fast cold starts, a small memory ceiling, and no Node APIs — which rules out most database drivers and any heavy work. Choose per route with an exported runtime constant.
Are Server Actions secure by default?
No. They compile to a reachable endpoint, so anyone can invoke one regardless of what your UI renders. Every action must authenticate the caller and validate its input exactly as a public endpoint would. Treating an action as internal because only your components call it is the main security mistake here.
Why does my database connection pool cause problems in Next.js?
Because in a serverless deployment each function instance is separate and short-lived, so each opens its own pool and a traffic burst exhausts the database’s connection limit. A long-running Node server holds one pool for its lifetime and avoids this. Which you get depends on where you deploy, so decide before designing the data layer.