Deploy a Django API With a Next.js Frontend
Deploy a Django REST or Ninja API with a separate Next.js frontend: same-domain routing versus subdomains, CORS and CSRF for cookie auth, server-side fetches from Next.js to Django, environment URLs, and where each part should be hosted.
Problem statement
A common modern stack pairs Django (for the API, admin, and business logic) with Next.js (for server-rendered and interactive pages). In development both run on localhost and talk freely. In production, decisions pile up: should the API live at example.com/api or api.example.com, how do cookies and CSRF work across them, should Next.js call Django from the browser or from its server, and where should each run? Getting those wrong produces CORS errors, users logged out on refresh, and slow server-side rendering.
- Prefer one domain: route
/api/and/admin/to Django and everything else to Next.js at the proxy (Nginx, Caddy, or a load balancer). - With one domain, session cookies and CSRF work without CORS; with subdomains, configure CORS,
SameSite, and cookie domains. - For server-side rendering, let Next.js call Django on an internal URL (
http://django:8000) and forward the user's cookies. - Expose the public API URL to the browser through
NEXT_PUBLIC_API_URL, and the internal one through a server-only variable. - Run Django with Gunicorn and Next.js with
next start(or a Node host such as Vercel), each with health checks. - Version and deploy them independently, keeping the API backward compatible across releases.
Step-by-step solution
1. Route one domain to both apps
server {
listen 443 ssl;
server_name example.com;
location /api/ { proxy_pass http://127.0.0.1:8000; include proxy_params; }
location /admin/ { proxy_pass http://127.0.0.1:8000; include proxy_params; }
location /static/ { alias /srv/django/staticfiles/; }
location / {
proxy_pass http://127.0.0.1:3000; # next start
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
Django's static files need a distinct prefix from Next.js assets (/_next/), which is naturally the case. Change Django's STATIC_URL to /django-static/ if anything collides.
2. Authentication and CSRF
With one domain, Django's session cookie and csrftoken are first-party. The frontend reads csrftoken and sends it as X-CSRFToken on unsafe requests:
const csrf = document.cookie.split("; ").find(c => c.startsWith("csrftoken="))?.split("=")[1];
await fetch("/api/orders/", { method: "POST", headers: { "X-CSRFToken": csrf, "Content-Type": "application/json" }, credentials: "same-origin", body });
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
SESSION_COOKIE_SECURE = CSRF_COOKIE_SECURE = True
With api.example.com, you also need django-cors-headers with CORS_ALLOWED_ORIGINS and CORS_ALLOW_CREDENTIALS = True, cookies scoped with SESSION_COOKIE_DOMAIN = ".example.com", and credentials: "include" in fetches. Token auth (JWT) avoids cookies but needs secure storage and refresh handling.
3. Server-side rendering calls
When Next.js renders on the server, it should call Django directly on the internal network, not through the public internet, and forward the user's cookies so Django knows who is asking:
// app/orders/page.tsx (Next.js App Router, server component)
import { cookies } from "next/headers";
export default async function Orders() {
const cookieStore = await cookies(); // async in Next.js 15+
const res = await fetch(`${process.env.DJANGO_INTERNAL_URL}/api/orders/`, {
headers: { Cookie: cookieStore.toString(), Host: "example.com" },
cache: "no-store",
});
const orders = await res.json();
return <OrderList orders={orders} />;
}
Sending Host: example.com keeps Django's ALLOWED_HOSTS and absolute URLs consistent. Use NEXT_PUBLIC_API_URL only for browser-side calls; server-only variables never reach the client bundle.
4. Hosting options
- Single VPS or Compose stack: Nginx, Gunicorn (Django),
next start(Node), PostgreSQL, Redis. Simple and same-domain by default. - Split hosting: Next.js on Vercel, Django on a container platform. Use a custom domain for both and route
/apiwith Vercel rewrites to Django, which keeps the browser on one origin. - Kubernetes: two Deployments behind one Ingress with path-based rules.
5. Deploy independently, stay compatible
Frontend and backend deploy at different times, so the API must accept requests from the previous frontend version for a while: add fields rather than renaming them, and remove old fields only after the frontend no longer uses them.
Verification check: log in through the Next.js UI, refresh a server-rendered page and confirm you stay logged in, submit a form (POST) without CSRF errors, and confirm in the browser network tab that API calls go to example.com/api/ with no CORS preflight.
Explanation
Browsers treat each scheme, host, and port combination as a separate origin. Keeping Next.js and Django under one origin makes the API same-origin from the browser's point of view, so cookies are first-party and no CORS negotiation happens. Server-side rendering adds a second client, the Next.js server, which must talk to Django on behalf of the user; forwarding cookies preserves the user's session, and using the internal address avoids extra latency and public routing. Treating the two apps as independently deployed services then only requires a compatible API contract.
Edge cases / notes
- Next.js middleware can rewrite
/apito Django as an alternative to proxy rules, at the cost of extra hops. - Image optimisation in Next.js fetches media from Django or object storage; allow those hosts in
next.config.js. - WebSockets to Django Channels need the upgrade headers on the Django route too.
- Admin static files must still be collected and served by Django's side.
Next steps
- A React SPA without SSR: Serve Django and a React SPA on the Same Domain.
- If you choose subdomains, read Fix Django CORS Errors in Production.
FAQ
Should the Django API be on a subdomain or a path?
A path such as /api on the same domain is simpler: no CORS and first-party cookies. Use a subdomain when several clients share the API.
How does Next.js authenticate to Django during SSR?
By forwarding the user's cookies (session and CSRF) on the server-side fetch to Django's internal URL.
Can Next.js be on Vercel and Django elsewhere?
Yes. Use rewrites so the browser still calls /api on your domain, which Vercel forwards to Django.
Why do I get CSRF errors on POST from Next.js?
The request lacks the X-CSRFToken header or the origin is not in CSRF_TRUSTED_ORIGINS.