Next.js and FastAPI is one of my favourite full-stack combinations. TypeScript on the frontend, Python on the backend — you get the entire Python ML/AI ecosystem with a production-grade React framework. But wiring them together properly takes more thought than most tutorials show.
Here's the pattern I've refined across multiple projects.
Project Structure
/
├── frontend/ # Next.js app
│ ├── app/
│ ├── lib/api.ts # typed API client
│ └── middleware.ts # auth forwarding
├── backend/ # FastAPI app
│ ├── main.py
│ ├── routers/
│ └── schemas/ # Pydantic models
└── docker-compose.yml
The key discipline: the frontend never talks to the database. All data access goes through FastAPI, even for simple lookups.
Type-Safe API Client
Generate types from your FastAPI OpenAPI spec rather than writing them by hand. I use openapi-typescript:
npx openapi-typescript http://localhost:8000/openapi.json -o lib/api-types.ts
Then wrap the fetch calls:
// lib/api.ts
import type { paths } from "./api-types";
const BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
export async function apiFetch<T>(
path: string,
options?: RequestInit,
): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
if (!res.ok) {
const error = await res.json().catch(() => ({}));
throw new Error(error.detail ?? `HTTP ${res.status}`);
}
return res.json();
}
Run openapi-typescript as part of your dev startup script — you always have fresh types.
Auth: JWT + HTTP-Only Cookies
Don't store JWTs in localStorage. Use HTTP-only cookies — they're inaccessible to JavaScript and safe from XSS.
FastAPI side:
from fastapi import Response
from jose import jwt
@router.post("/auth/login")
async def login(credentials: LoginSchema, response: Response):
user = await authenticate(credentials)
token = jwt.encode({"sub": str(user.id)}, SECRET_KEY, algorithm="HS256")
response.set_cookie(
key="access_token",
value=token,
httponly=True,
secure=True, # HTTPS only in prod
samesite="lax",
max_age=60 * 60 * 24 * 7, # 7 days
)
return {"ok": True}
Next.js middleware: Forward the cookie on every server-side request:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("access_token")?.value;
const requestHeaders = new Headers(request.headers);
if (token) {
requestHeaders.set("Authorization", `Bearer ${token}`);
}
return NextResponse.next({ request: { headers: requestHeaders } });
}
Server Actions for Mutations
With Next.js 14+ Server Actions, you can call FastAPI directly from the server without an intermediate API route:
"use server";
import { cookies } from "next/headers";
export async function updateProfile(formData: FormData) {
const cookieStore = cookies();
const token = cookieStore.get("access_token")?.value;
const res = await fetch(`${process.env.API_URL}/users/me`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formData.get("name"),
bio: formData.get("bio"),
}),
});
if (!res.ok) throw new Error("Update failed");
}
This keeps your secrets server-side and your code colocated with the components that use it.
Deployment on the Same Domain
The cleanest production setup: both apps on the same domain via a reverse proxy. This eliminates all CORS headaches.
# nginx.conf
server {
listen 80;
server_name yourapp.com;
# Frontend
location / {
proxy_pass http://nextjs:3000;
}
# Backend — all /api/ routes
location /api/ {
proxy_pass http://fastapi:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
With Docker Compose, nextjs and fastapi are service names that resolve internally. Nothing is exposed directly; everything goes through nginx.
# docker-compose.yml (simplified)
services:
nginx:
image: nginx:alpine
ports: ["80:80"]
volumes: ["./nginx.conf:/etc/nginx/conf.d/default.conf"]
nextjs:
build: ./frontend
environment:
NEXT_PUBLIC_API_URL: /api
fastapi:
build: ./backend
The Pattern in One Sentence
Keep your types generated, your secrets server-side, your auth in HTTP-only cookies, and your apps behind a single reverse proxy — and the Next.js + FastAPI stack will take you surprisingly far.