Calling a Separate Backend
AdvancedWhen the real API is FastAPI or Spring Boot, Next becomes a client of it — and the choice of whether the browser or the Next server makes that call changes auth, CORS and latency.
Overview
A great deal of production Next.js is not full-stack Next at all: the business logic lives in a Python or Java service and Next renders the frontend. That makes Next a consumer of your API, and it introduces a decision the previous concepts did not have — does the browser call the backend directly, or does the Next server call it and render the result? The answer changes where the token lives, whether CORS applies at all, and how many network hops a page costs. This is the Full-Stack Integration track, seen from inside the framework.
Two Topologies
Browser-to-API, or browser-to-Next-to-API.
// A. The browser calls the backend directly
// browser -> api.aicancode.org (FastAPI on Fly)
// + one hop, no Next server involved
// - CORS applies, the token must be reachable by JS or the cookie
// must be same-site, and nothing is server-rendered
NEXT_PUBLIC_API_URL=https://api.aicancode.org
// B. The Next server calls the backend, then renders
// browser -> Next (Vercel) -> api.aicancode.org
// + no CORS at all (server to server), the token never reaches the
// browser, HTML arrives ready to read, SEO works
// - an extra hop, and Vercel and Fly should be in the same region
// or you pay for the distance twice
API_URL=https://api.aicancode.org // server-only, no NEXT_PUBLIC_
// Most apps use both: B for the initial render and anything secret,
// A for interactive updates after the page is live.
// A third option worth knowing — proxy through Next so the browser
// only ever sees one origin:
// next.config.js
async rewrites() {
return [{ source: '/api/:path*', destination: `${process.env.API_URL}/:path*` }]
}
// Cookies become first-party and CORS disappears, at the cost of
// routing traffic through Vercel.Forwarding the Session
The mistake that makes a server-rendered page anonymous.
// A server component has no browser attached, so nothing is sent
// automatically. This call is ANONYMOUS:
const res = await fetch(`${process.env.API_URL}/me`) // 401
// Forward the cookie explicitly:
import { cookies } from 'next/headers'
export async function apiFetch(path: string, init: RequestInit = {}) {
const res = await fetch(`${process.env.API_URL}${path}`, {
...init,
headers: {
...init.headers,
cookie: cookies().toString(), // the user's session
'X-Request-Id': crypto.randomUUID(), // correlate the logs
},
cache: 'no-store', // per-user: never cache
})
if (!res.ok) throw new ApiError(res.status, await res.text())
return res.json()
}
// Two things this makes true at once:
// - calling cookies() makes the route dynamic, which is correct for
// per-user data
// - 'no-store' keeps one user's response out of another's page
// For service-to-service calls with no user, send a service token
// instead — and never a NEXT_PUBLIC_ one:
headers: { Authorization: `Bearer ${process.env.SERVICE_TOKEN}` }Keeping the Two Deployables Honest
Types, errors and the failure modes of the extra hop.
# Generate the client types from the backend's OpenAPI schema
npx openapi-typescript $API_URL/openapi.json -o src/generated/api.ts
# and fail CI on drift, so a renamed field breaks the build rather
# than production. (The Full-Stack Integration track covers the why.)
// Timeouts: the Next server calling a slow backend will hold a
// serverless function open and eventually hit the platform limit.
// Always bound it:
fetch(url, { signal: AbortSignal.timeout(8000) })
// and budget downward: browser 10s > Next 8s > backend 5s.
// A backend outage should degrade, not blank the page:
try {
const stats = await apiFetch('/stats')
return <Stats data={stats} />
} catch {
return <StatsUnavailable /> // the rest of the page survives
}
// Or let it throw and let error.tsx contain it to that segment.
// Cold starts compound: Vercel wakes, then Fly wakes. Keep
// min_machines_running = 1 on the backend for anything user-facing,
// or the first visitor after a quiet period waits for both.Key Points to Remember
- 1Calling the backend from the Next server removes CORS entirely and keeps the token off the browser, at the cost of a hop
- 2A server component sends no cookies automatically — forward them explicitly or the request is anonymous
- 3Per-user responses must use no-store, or one user's data can be served inside another user's page
- 4Generate client types from the backend's OpenAPI schema and fail CI on drift
- 5Bound every outbound call with a timeout and degrade gracefully, since two platforms mean two cold starts
Interview Questions
Sign in to ask AriaWhat changes when the Next server calls your API instead of the browser?
Why does an authenticated fetch in a server component return 401?
How do you stop a slow backend from hanging a server-rendered page?
Ask Aria about Calling a Separate Backend
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.