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.

Next.js plus Django APIBrowserexample.comNext.js serverSSR, route handlersDjango API/api on same domainPostgres + CeleryDjango sideOne domain keeps cookies and CSRF simple; Next.js calls Django internally when rendering.
  1. Prefer one domain: route /api/ and /admin/ to Django and everything else to Next.js at the proxy (Nginx, Caddy, or a load balancer).
  2. With one domain, session cookies and CSRF work without CORS; with subdomains, configure CORS, SameSite, and cookie domains.
  3. For server-side rendering, let Next.js call Django on an internal URL (http://django:8000) and forward the user's cookies.
  4. Expose the public API URL to the browser through NEXT_PUBLIC_API_URL, and the internal one through a server-only variable.
  5. Run Django with Gunicorn and Next.js with next start (or a Node host such as Vercel), each with health checks.
  6. 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.

where should the API live?example.com/api✓first-party cookies, no CORS✓CSRF token read from cookie✓one certificate, one originapi.example.comCORS with credentials neededcookie domain .example.com✕more ways to misconfigureSame-domain routing removes a whole class of cookie and CORS bugsdjango-deployment.com
Subdomains make sense when the API serves several clients, such as mobile apps.

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 /api with 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.

Next.js + Django in productionPath-based routing on one domain/api and /admin to DjangoCSRF header on unsafe requestscsrftoken cookieSSR fetches use the internal URLcookies and Host forwardedPublic and server-only env vars splitNEXT_PUBLIC_ only for browserBackward-compatible API changesindependent deploysMost production bugs in this stack are routing, cookies, or env URLsdjango-deployment.com
Expose a version endpoint on both apps to see what is deployed.

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 /api to 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

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.

Keep going