Updated Nov 5, 20266 min read

Next.js Backend: When Route Handlers Are Enough and When to Split Out an API

Next.js can serve API routes and Server Actions, and many apps outgrow that once they need queues, cron, long-running work or a second client. How far the built-in backend goes, the signs it is time for a separate backend, and how to connect one with a typed client.

Next.js Route Handlers serve HTTP endpoints from the app directory, Server Actions run mutations from components, and server components query databases directly, so for many products the first version never needs a separate API.

The question comes up once the app has work that does not fit a request-response cycle in a frontend framework: a job that runs every night, an email that should go out after a payment webhook, a mobile app that needs the same endpoints, or a second team that owns the data. This guide covers what the Next.js backend handles well, the signs it is time to split one out, and how to connect a separate TypeScript backend with a typed client.

Next.js backend options at a glance

Next.js Route HandlersNext.js Server ActionsSeparate backend (Encore)
What it isHTTP endpoints in route.ts"use server" functions called from componentsTypeScript services with typed APIs
Callable byAny HTTP clientYour own React componentsAny client, plus a generated typed client
Background jobs and queuesNot built inNot built inPub/Sub topics and subscriptions
Scheduled workExternal schedulerNoCron jobs declared in code
DatabasesBring your own client and connection poolingSamePostgres declared in code, local and in the cloud
Runs onWherever Next.js is hostedSameYour AWS or GCP account

What the Next.js backend handles well

Route Handlers export functions named after HTTP methods from a route.ts file and use the Web Request and Response APIs. They suit webhooks, small JSON APIs for your own frontend, OAuth callbacks and proxying to third-party APIs:

// app/api/todos/route.ts import { NextResponse } from "next/server"; export async function GET() { const todos = [{ id: 1, title: "Ship the launch post", done: false }]; return NextResponse.json(todos); }

Server Actions remove the endpoint entirely for mutations from your own UI: a form posts to an async function that runs on the server and can revalidate the page. Together with server components that read data directly, this covers CRUD apps, dashboards and content sites without a separate API layer.

Where it starts to strain

Next.js is a rendering framework usually deployed as serverless functions, which limits it in these ways:

  • Work outside a request. Sending an email after a webhook, resizing an upload or syncing a CRM should not block the response. Next.js has after() for small follow-up work, and retries, queues and dead-letter handling are outside its scope.
  • Scheduled jobs. Cron needs a scheduler from the hosting platform or an external service that calls a route.
  • Execution limits. Serverless functions have maximum durations and memory set by the host, which rules out long reports and large imports.
  • Database connections. Every function instance opens its own connections, so Postgres usually needs a pooler in front.
  • More than one client. Once a mobile app or partner integrations call the API, Server Actions are no longer an option and the API needs versioning and documentation of its own.
  • Ownership. A backend team owning databases, queues and infrastructure has to work inside the frontend's repo and deploy cycle.

When two or three of these apply, a separate backend with Next.js as its client is usually simpler than stretching the frontend framework.

Encore as the backend for a Next.js app

Encore

Encore is a tool that works alongside your framework for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. The backend is TypeScript services with typed endpoints, and databases, Pub/Sub, cron jobs and buckets are declared in the same code, so the parts Next.js leaves out are part of the backend from the start. Next.js stays on whatever host you use for the frontend, as the frontend hosting docs describe.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.

Set up the app

The Next.js starter contains both the Encore backend and the Next.js frontend in one repo:

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create my-app --example=ts/nextjs-starter$ cd my-app && encore run

A backend service with an endpoint and a database:

// api/todo/encore.service.ts import { Service } from "encore.dev/service"; export default new Service("todo");
// api/todo/todo.ts import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; const db = new SQLDatabase("todo", { migrations: "./migrations" }); interface Todo { id: number; title: string; done: boolean; } export const list = api( { expose: true, method: "GET", path: "/todos" }, async (): Promise<{ todos: Todo[] }> => { const todos: Todo[] = []; for await (const row of db.query<Todo>`SELECT id, title, done FROM todo ORDER BY id`) { todos.push(row); } return { todos }; }, );
-- api/todo/migrations/1_create_todo.up.sql CREATE TABLE todo ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false );

Generate a typed client from the running backend, then call it from a server component in Next.js:

~/my-app
$ encore gen client --output=./app/lib/client.ts --env=local
// app/todos/page.tsx import Client, { Environment, Local } from "../lib/client"; const encoreEnv = process.env.ENCORE_ENVIRONMENT; const client = new Client( process.env.NODE_ENV === "development" || !encoreEnv ? Local : Environment(encoreEnv), ); export default async function TodosPage() { const { todos } = await client.todo.list(); return ( <ul> {todos.map((t) => ( <li key={t.id}>{t.done ? "✓" : "○"} {t.title}</li> ))} </ul> ); }

The client carries the request and response types, so renaming a field in the backend breaks the Next.js build instead of failing at runtime. Regenerating it is covered in the request client docs and client generation. encore run starts Postgres locally, and on deploy Encore provisions the database in your AWS or GCP account. Browser calls from another origin are allowed through the global_cors setting in encore.app, as described in the CORS docs.

For AI coding agents the split helps: an agent changing an endpoint regenerates the client and gets type errors in the exact Next.js files that need updating, across both halves of the repo.

Best fit: Next.js teams whose backend needs queues, cron, multiple clients or its own deploy cycle, and who want it in TypeScript with types shared to the frontend.

Good to know

  • Keep Server Actions for UI mutations even with a separate backend, calling the typed client from inside the action.
  • Point the client at Environment("staging") or a production URL through configuration, and keep Local for development.
  • Webhooks from payment providers usually belong in the backend, where retries and queues are available.

Frequently asked questions

Can Next.js be used as a backend?

Yes, for a lot of apps. Route Handlers serve HTTP endpoints from the app directory and Server Actions run server-side mutations called from components. Next.js is weaker at background jobs, queues, scheduled work, long-lived connections and serving several clients, which is when teams add a separate backend.

What is the difference between Route Handlers and Server Actions?

Route Handlers are HTTP endpoints defined in route.ts files that export functions such as GET and POST, callable by any client. Server Actions are async functions marked with "use server" that React components call directly, mainly for form submissions and mutations from your own UI.

Should I use a separate backend with Next.js?

Use one when the backend has its own lifecycle: a mobile app or partners call the same API, work runs in the background or on a schedule, services communicate through queues, or the database and infrastructure are owned by a backend team. Keep Next.js for rendering and the UI-facing data fetching.

How do I connect a Next.js frontend to a TypeScript backend with type safety?

With Encore, define endpoints with api() from encore.dev/api, then run encore gen client --output=./app/lib/client.ts --env=local to generate a typed client. Next.js server components and Server Actions import the client and call endpoints such as client.todo.list() with full request and response types.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders