All Posts

20 September 2026

Next.js 15 + FastAPI: The Full-Stack Architecture I Use in Production

Next.jsFastAPIFull-StackArchitecture

A lot of people ask why I don't just use Next.js API routes for everything instead of running a separate FastAPI backend. Here's the actual reasoning, based on shipping several full-stack projects with this exact split.

The Split

  • Next.js (frontend) — rendering, routing, auth sessions (via NextAuth), SEO metadata, and anything that benefits from React Server Components.
  • FastAPI (backend) — the real business logic, database access via SQLAlchemy, and anything Python-specific: ML inference, YouTube API sync, PDF/image processing, LLM calls.

They communicate over plain HTTP/JSON. The frontend never talks to the database directly.

Browser → Next.js (SSR/CSR) → FastAPI (/api/*) → PostgreSQL

Why Not Just Use Next.js API Routes?

Next.js API routes are genuinely good for thin proxying or simple CRUD. The reason I still reach for FastAPI:

  1. Python's ecosystem for AI/ML is unmatched. If a feature needs opencv, yt-dlp, a Gemini/OpenAI SDK, or any ML library, doing that in a Node.js API route means fighting the ecosystem the whole way.
  2. SQLAlchemy + Pydantic gives you real schema validation on both the database and API layers, which scales better than hand-rolled validation as the project grows.
  3. Separation of deploys. The backend can scale, restart, or crash independently of the frontend. On a small EC2 box this also means you can restart one PM2 process without taking down the other.

How Auth Works Across the Split

This is the part people get stuck on. In this architecture:

  • NextAuth handles the session (cookies, JWT, OAuth flow with Google/GitHub) entirely inside the Next.js app.
  • For requests that need backend data, the Next.js server (not the browser) calls FastAPI with a server-only admin key or the user's identifier, over server actions or route handlers.
  • Sensitive keys (BACKEND_ADMIN_KEY, database URL, etc.) live only in server-side env vars — never in NEXT_PUBLIC_* variables, which get baked into the client-side JS bundle and are visible to anyone.
// Next.js server action — never runs in the browser
async function getAdminData() {
  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/admin/x`, {
    headers: { "X-Admin-Key": process.env.BACKEND_ADMIN_KEY! },
  });
  return res.json();
}

Deployment Shape

In production this becomes two separate processes behind one Nginx server, each on its own subdomain:

codewithmunnax.com        → Nginx → Next.js  (port 3004)
api.codewithmunnax.com    → Nginx → FastAPI  (port 8004)

Nginx terminates SSL for both and reverse-proxies to whichever internal port the process manager (PM2, in my case) is running. Keeping the API on its own subdomain also makes CORS configuration explicit and easy to reason about, rather than everything sharing one origin.

When This Split Isn't Worth It

If your project has no Python-specific need (no ML, no heavy data processing, no yt-dlp/OpenCV-style dependency) and is mostly CRUD, a single Next.js app with API routes and an ORM like Prisma is simpler to run and deploy. Don't add a second backend just because it looks more "senior" — add it when you have a concrete reason to.

FAQ

Common Questions

Only if you have a concrete reason — usually Python-specific libraries (ML, data processing, video/image tooling) that don't have good Node.js equivalents. Otherwise a single Next.js app is simpler to deploy and maintain.