Building a Next.js Frontend with a Django REST API
A practical architecture guide to the decoupled Next.js + Django REST pattern — auth, CORS, data fetching, deployment, and the pitfalls that quietly cost teams a week.
On this page
Building a Next.js frontend with a Django REST API means running two independent applications — a React-based frontend and a Python backend — that talk to each other over HTTP instead of living inside one monolith. The frontend renders the UI and handles routing; Django, usually paired with Django REST Framework, owns the database, business logic, and authentication, and exposes all of it as JSON endpoints. This decoupled architecture is what we reach for on most product builds.
The appeal is clean separation. Your React and Python work can move on their own tracks, you get a fast and SEO-friendly frontend, and you keep Django's mature admin, ORM, and auth stack. The cost is that you now own the seam between the two apps — authentication, CORS, data fetching, and deployment all become deliberate decisions instead of framework defaults. Get the seam right and the pattern is a joy to work in. Get it wrong and you spend a week chasing preflight errors and phantom 401s.
This guide walks the whole seam, engineer to engineer but without assuming you have shipped this exact stack before. It covers the architecture, the auth decision that trips up most teams, CORS, where data should actually be fetched with the App Router, how to shape the Django API, deployment, and the pitfalls we watch for on every project.
- A decoupled Next.js + Django REST setup is two deployable apps joined by an HTTP contract — treat that contract as a first-class deliverable, not an afterthought.
- Session cookies suit same-site, first-party apps; JWT suits multiple clients (web plus mobile). Never store access tokens in
localStorageif you can avoid it. - Most CORS pain disappears if the browser talks to Next.js route handlers and Next.js talks to Django server-to-server.
- Use Server Components for SEO and first paint; use client components with React Query or SWR for interactive, per-user data.
- Deploy the two apps separately, keep them on the same parent domain where you can, and make every base URL an environment variable.

What does a decoupled Next.js + Django REST architecture actually look like?
At the highest level there are three moving parts: the Next.js app (React components, routing, server-side rendering), the Django + DRF app (models, serializers, views, auth), and the API contract that defines every request and response between them. In a monolith the framework hides the contract from you. Here it is explicit, and that explicitness is the whole point — it is also the whole risk.
The two halves and their responsibilities
Django is the source of truth. It talks to Postgres, enforces business rules, runs background jobs, and serves JSON. Next.js never touches the database directly; it asks Django for data and posts changes back. This is the boundary that lets a Django development team and a frontend team work in parallel without stepping on each other, and it is the same boundary that lets you later add a Flutter mobile app that reuses the exact same endpoints.
The contract is the product
The endpoints, their shapes, their error formats, their pagination, and their auth rules are the real interface of your system. When we scope a build, the API contract gets designed early and written down — often before much UI exists — because every screen, every loading state, and every retry depends on it. If you treat it casually, the two teams drift, and you discover the mismatch during integration when it is most expensive to fix. This is exactly the discipline that separates smooth web application development from projects that stall at the halfway mark.
Document the contract in something machine-readable. DRF can emit an OpenAPI schema, and you can generate a typed client for Next.js from it. A typed client turns a whole class of integration bugs into TypeScript errors you catch before they ship.
Should you use session auth or JWT with a Next.js frontend?
This is the decision that causes the most confusion, and there is no single right answer — there is a right answer for your topology. The two mainstream options are Django's built-in session cookies and JSON Web Tokens (commonly via the SimpleJWT package for DRF). The question you are really answering is: how many clients will hit this API, and do they share a domain?
Session cookies: simplest when everything is first-party
If your Next.js app and your Django API live on the same parent domain — say the app on app.example.com and the API on api.example.com — session cookies are the least surprising choice. Django manages a server-side session, sets an HttpOnly cookie the browser cannot read from JavaScript, and every request carries it automatically. Revocation is trivial: delete the session row and the user is out. The trade-off is that you must handle CSRF deliberately and get your SameSite cookie attributes right, especially if the frontend and API are on different subdomains.
JWT: better when you have multiple clients
If a mobile app, a third-party integration, or several frontends all consume the same API, stateless JWT access tokens are easier to reason about. The token carries the identity, so any service can verify it without a shared session store. The catch is revocation — a signed token is valid until it expires, so short access-token lifetimes plus a refresh-token flow (and often a server-side blocklist) are non-negotiable. This is the model we lean toward when a project's API development and integration scope clearly includes more than one client from day one.
Do not store access tokens in localStorage. Anything readable by JavaScript is readable by an XSS payload. Prefer an HttpOnly cookie set by a Next.js route handler, or keep the access token in memory and the refresh token in an HttpOnly cookie. Convenience here is a security liability.

| Dimension | Session cookies (Django) | JWT (DRF SimpleJWT) |
|---|---|---|
| Where state lives | Server-side session store | Stateless, inside the token |
| Browser storage | HttpOnly cookie | Cookie or memory (avoid localStorage) |
| Revocation | Immediate — delete the session | Needs short expiry + refresh/blocklist |
| CSRF exposure | Yes — needs CSRF handling | Lower when not cookie-based |
| Cross-domain use | Harder (SameSite/Secure) | Easier |
| Best fit | Same-site, first-party web app | Web + mobile + third parties |
If you only take one thing from this section: pick based on how many clients share your domain, then make the token or cookie unreadable to JavaScript. Everything else is detail. If you are still choosing your stack more broadly, our breakdown of how we choose the right tech stack covers the client-count question in context.
How do you fix CORS between Next.js and Django?
CORS (Cross-Origin Resource Sharing) is the browser rule that blocks a page on one origin from reading a response from another origin unless the server opts in. When your Next.js browser code calls Django on a different origin, the browser sends a preflight OPTIONS request, and Django has to answer with the right Access-Control-Allow-* headers or the real request never fires. Nearly every "it works in Postman but not the browser" bug lives here.
The direct-browser-to-Django approach
The standard fix on the Django side is the django-cors-headers package. You add its middleware and explicitly allow-list your frontend origin — never use a wildcard when credentials are involved, because the browser refuses to combine Access-Control-Allow-Origin: * with cookies. If you use cookie-based auth, you also set CORS_ALLOW_CREDENTIALS = True and the frontend must send requests with credentials included. Be precise: the allowed origin must match scheme, host, and port exactly.
The approach we usually prefer: proxy through Next.js
The cleaner pattern for many builds is to make the browser talk only to your Next.js route handlers, and have those handlers call Django server-to-server. Server-to-server requests are not subject to CORS at all, so an entire category of preflight problems evaporates. As a bonus, tokens and secrets stay on the server, and you get a natural place to add caching, rate limiting, and request shaping. The trade-off is one extra network hop and a little more code, which is usually a bargain.
Proxying also sidesteps mixed same-site cookie headaches. If the browser only ever sees your own domain, cookies are first-party by definition and SameSite=Lax "just works" — no SameSite=None; Secure gymnastics required.
Server Components vs client fetching: where should data come from?
The Next.js App Router gives you two very different places to fetch data, and choosing well is what makes a decoupled app feel fast. Server Components run on the server, fetch from Django before the HTML is sent, and never ship that data-fetching code or your API tokens to the browser. Client Components run in the browser and fetch after hydration, which is what you want for interactive, per-user, frequently-changing data.
The hard part of a decoupled build is not React or Django — it is the contract between them, and whoever owns that seam owns the product's reliability.
Fetch on the server for SEO and first paint
Public, indexable pages — marketing content, product listings, blog articles — should be rendered on the server so crawlers and first-time visitors get real HTML immediately. Fetch from Django inside a Server Component, and the user's browser never has to make a round trip before seeing content. One caveat that catches teams upgrading between versions: the caching default for fetch changed in recent Next.js releases, so requests are no longer implicitly cached the way they once were. Be explicit about caching and revalidation rather than relying on a default that may differ across versions.
Fetch on the client for interactive, per-user data
Dashboards, settings pages, and anything behind a login are usually better fetched on the client with a data library like React Query or SWR. These give you caching, background refetching, retries, and optimistic updates for free, which is exactly what a live app needs. The data is private anyway, so there is little SEO benefit to server-rendering it, and client fetching keeps per-user tokens off the shared server render path.
| What you are building | Where to fetch | Why |
|---|---|---|
| SEO-critical public page | Server Component | Real HTML on first paint, no token in browser |
| Private dashboard | Client + React Query/SWR | Interactive, per-user, cached on the client |
| Frequently changing list | Client with revalidation | Background refetch keeps it fresh |
| One-off mutation | Server Action or route handler | Keeps secrets and logic server-side |
How do you structure the Django REST API for a Next.js app?
A frontend is only as pleasant as the API behind it. A few DRF conventions make the whole integration smoother, and they cost almost nothing to set up early.
Serializers, pagination, and a stable error shape
Use DRF serializers as the single place where your data shape is defined, and turn on pagination from the first endpoint — retrofitting it after the frontend assumes unbounded arrays is painful. Just as important, standardize your error responses. When every failure comes back in the same JSON shape with a consistent field for messages, the frontend can write one error handler instead of a special case per endpoint. Inconsistent error formats are one of the quiet reasons integration drags on.
Versioning and thin views
Put your API under a version prefix such as /api/v1/ from day one. It costs nothing now and saves a migration nightmare later when you need to change a response without breaking existing clients. Keep views thin — validation in serializers, business logic in services or models — so the API stays testable as it grows. These are the habits that let a SaaS platform we built in 8 weeks keep shipping features without the backend turning into a tangle.

Deploying a decoupled Next.js + Django app
Two apps means two deployments, and the friction is almost always in how they find and trust each other. The good news is that the rules are boring once you know them.
Separate hosts, one parent domain
Next.js typically runs on a Node-friendly platform or container; Django runs behind a WSGI/ASGI server such as Gunicorn or Uvicorn, usually with a reverse proxy in front. Keep them on the same parent domain where you can — frontend on app.example.com, API on api.example.com — so cookies stay first-party and your CORS and SameSite config stays simple. When you cannot, the Next.js proxy pattern from earlier is the reliable fallback.
Every base URL is an environment variable
Hardcoding http://localhost:8000 anywhere is a future outage. The Django base URL, the frontend's public URL, the allowed CORS origins, and your secrets all belong in environment variables so the same build promotes cleanly from local to staging to production. Remember the split in Next.js between server-only variables and the NEXT_PUBLIC_ prefix that ships to the browser — put API secrets in the former, never the latter. Static files, database URLs, and CORS allow-lists round out the checklist. When we hand a brief to a client, this env matrix is part of it; if you are writing your own spec, our software project brief template captures the same fields so nothing gets discovered in production.
What are the most common pitfalls in this stack?
These are the issues we actively design against, because each one has cost real teams real days:
- Tokens in
localStorage. Convenient, and a direct XSS-to-account-takeover path. UseHttpOnlycookies or in-memory storage. - Wildcard CORS with credentials. Browsers refuse it, and loosening the config further to "fix" it opens a real hole. Allow-list explicit origins instead.
- Trailing-slash mismatches. Django's
APPEND_SLASHplus a frontend that omits the slash produces mysterious redirects that dropPOSTbodies. Standardize on one convention. - Assuming a caching default. Relying on Next.js
fetchcaching behavior that changed between versions leads to either stale data or missing performance. Be explicit. - Skipping pagination. An endpoint that returns every row is fine with 20 records and a crisis with 200,000. Paginate from the start.
- No shared error contract. When each endpoint fails differently, the frontend fills up with special cases. Define one error shape and enforce it.
None of these are exotic. They are exactly the boring, avoidable failures that a decoupled architecture surfaces because the seam is exposed — which is why owning the seam deliberately is the entire discipline. Ongoing vigilance here is part of why we treat application maintenance and support as a first-class phase, not an afterthought.
You can stand up a Next.js + Django prototype in an afternoon. The gap between that prototype and something you can safely put in front of paying users is almost entirely in this list. Budget for it explicitly rather than discovering it at launch.
Frequently asked questions
Do I need Django REST Framework, or can I use plain Django?
You can serve JSON from plain Django views, but Django REST Framework gives you serializers, authentication classes, pagination, throttling, and browsable API docs out of the box. For anything beyond a handful of endpoints, DRF saves far more time than it costs, and it standardizes the patterns your Next.js frontend will depend on.
Should the Next.js app call Django directly from the browser, or proxy through Next.js?
Both work. Direct browser-to-Django calls need correct CORS and cookie configuration. Proxying through Next.js route handlers avoids CORS entirely, keeps secrets on the server, and gives you a place to add caching — at the cost of one extra hop. For most product builds we lean toward proxying because it removes a whole class of browser-only bugs.
Is JWT or session auth more secure for this stack?
Neither is inherently more secure; the risk is in how you store the credential. An HttpOnly session cookie and an HttpOnly refresh-token cookie are both strong. A JWT sitting in localStorage is weak because JavaScript — including malicious injected scripts — can read it. Choose based on how many clients you serve, then store the credential where scripts cannot reach it.
Can I use Server Components with an authenticated Django API?
Yes. Read the auth cookie inside the Server Component or route handler and forward it to Django on the server side. Because the fetch happens on the server, the token never reaches the browser as readable JavaScript, which is one of the security advantages of server-side data fetching.
How do I handle CSRF with Django and a Next.js frontend?
If you use cookie-based session auth, CSRF protection matters: have Django issue a CSRF token and send it back on unsafe requests, and keep SameSite attributes strict. If you use token auth sent in an Authorization header rather than a cookie, CSRF risk is lower because the browser does not attach the header automatically. Match your CSRF strategy to your auth strategy.
Planning a build on this stack, or untangling a decoupled app that has started to fight you? Talk to our team — we design the Next.js and Django seam deliberately so your product stays fast, secure, and easy to extend, and we can review your current architecture or scope a new one with you.
Sources
Have a project like this in mind?
Tell us what you're building and we'll map out the scope, timeline and a fixed starting quote — no obligation.
Start your project


