Using Next.js as a backend means giving one frontend its own thin API layer, shaped around the screens that consume it instead of around a general-purpose data model. That layer is built from Route Handlers, alongside the Server Components and Server Functions that already run on the server.
The point is to keep credentials, aggregation, and payload shaping on the server, so the browser talks to one endpoint designed for it rather than to three upstream services.
A project that is only this layer can be scaffolded directly.
npx create-next-app@latest --apiThe api flag generates a project containing route handlers and no page UI, which fits a service that returns JSON and nothing else. Everything below applies equally to an API layer living inside a full Next.js app.
What belongs in the layer
Route Handlers are publicly reachable HTTP endpoints. Any client can call them, so each handler needs an explicit access decision and protected data requires authentication and authorization.
They earn their place when one of these is true.
- An upstream API needs a secret key that must never reach the browser.
- One screen needs data from several services and you want a single round trip.
- The response is not HTML or React output: a feed, a file, a redirect, or a callback target.
- The caller is outside your app, such as a webhook sender or a native client.
Choosing the right tool
Not everything needs an endpoint. Next.js gives you three server-side entry points, and picking the wrong one is the most common structural mistake in this pattern.
| Need | Use | Why |
|---|---|---|
| Render data on a page | Server Component | Queries the source directly, no HTTP hop |
| Mutate data from your own UI | Server Function | No separate Route Handler or fetch call to maintain |
| Serve an external or non-UI caller | Route Handler | A real URL with methods and status codes |
The comparison in server actions vs API routes vs route handlers goes deeper on the middle row. The rule that saves the most time is the first one: a Server Component should read from the database or upstream API directly.
Fetching your own Route Handler from a Server Component adds an HTTP round trip on every render, and during a build there is no server listening to answer it, so prerendering that route fails.
Aggregating several sources
This is the case where the pattern pays for itself. One handler calls two services in parallel and returns exactly the shape one screen needs.
// app/api/dashboard/route.ts
import { getCurrentUser } from '@/lib/auth'
import { getOrders, getUsage } from '@/lib/services'
export async function GET() {
const user = await getCurrentUser()
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 })
const [orders, usage] = await Promise.all([getOrders(user.id), getUsage(user.id)])
return Response.json({ orderCount: orders.length, usedCredits: usage.total })
}An authenticated client fetching /api/dashboard receives one small JSON object instead of making two requests and reducing the results itself. Both upstream calls happen server side, so their credentials and their full payloads never reach the browser. The project-specific getCurrentUser helper must verify the session on the server, not merely decode an untrusted cookie.
If this data is rendered by your own page, skip the endpoint and call the same two functions from the Server Component. The endpoint is worth adding when a client-side widget polls it, or when a separate app consumes it.
Proxying to an existing backend
A second common shape is forwarding requests to a backend that already exists, adding validation or auth on the way through.
// app/api/upstream/[...slug]/route.ts
import { isAllowed } from '@/lib/upstream'
type Ctx = RouteContext<'/api/upstream/[...slug]'>
export async function POST(request: Request, ctx: Ctx) {
const { slug } = await ctx.params
if (!isAllowed(slug)) return new Response(null, { status: 400 })
const url = new URL(slug.join('/'), process.env.UPSTREAM_ORIGIN!)
return fetch(new Request(url, request))
}Rejected paths get a 400 before anything leaves your server, and allowed ones are forwarded with their method, headers, and body intact. Keep the upstream origin fixed and trusted, and verify credentials here if the upstream does not do it. When no logic is needed at all, a rewrite in the config is cheaper than a handler, as covered in rewrites and proxying to an external API.
How the pieces sit together
The diagram below shows the shape of a request once this layer exists, with the browser talking to one origin.
Two paths coexist. Page rendering reads the database directly inside a Server Component, while the endpoint fans out to third-party services for callers that need JSON. Neither path sends a credential to the browser.
What this layer is not
Next.js backend capabilities are an API layer, not a full backend replacement, and the gap shows up in deployment rather than in development.
- Many hosts run handlers as functions, so they cannot share in-memory state between requests.
- On those hosts, the filesystem may be read-only, and writable temporary data may not persist between invocations.
- Long-running work can be terminated by an execution timeout.
- Persistent connections such as WebSockets are unsupported on hosts that close the connection when the response ends.
- In static export mode only GET handlers work, and only with a force-static route config, which belongs to the pre-Cache-Components caching model.
Queues, cron-driven jobs, and stateful services still belong in infrastructure built for them. The layer in front of them can still live here.
Security is not optional here
Every handler you add is a public URL. Treat it that way from the first commit.
- Verify credentials inside each protected handler rather than relying on proxy alone, since a matcher change can silently remove that coverage.
- Treat exported Server Functions as public entry points too, with authorization and input validation inside the function.
- Validate the content type, size, and shape of every payload before passing it on.
- Keep error messages generic so internal details do not leak to callers.
- Add limits to anything expensive, as described in rate limiting an API endpoint.
The short version
Add an API layer when something outside your React tree needs an HTTP endpoint, when credentials must stay server side, or when one screen needs several sources merged. Keep page data fetching inside Server Components, keep UI mutations in Server Functions, and treat every handler as a public endpoint. The file convention itself is covered in route handlers and route.js.
Rune AI
Key Insights
- A backend for frontend is an API layer shaped around one client's screens.
- Route Handlers are public endpoints and any client can reach them.
- Server Components should query data sources directly, not your own endpoints.
- Server Functions cover mutations from your own UI without an endpoint.
- Use the layer to hide credentials, aggregate services, and reshape payloads.
- Host limits on duration, shared state, and connections still apply.
Frequently Asked Questions
Should a Server Component fetch my own Route Handler?
Is Next.js a replacement for a real backend?
When should I use a Server Function instead of a Route Handler?
Can I scaffold a project that is only an API?
Conclusion
The backend-for-frontend pattern gives one frontend an API layer shaped around its screens, and in Next.js that layer is Route Handlers alongside Server Components and Server Functions. Reach for a Route Handler when a caller outside your React tree needs an HTTP endpoint, keep internal data fetching inside Server Components, and remember this layer runs under your host's function limits.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.