Django and Next.js are a popular pair: Django brings a mature ORM, migrations, an admin panel, and a large Python ecosystem, while Next.js brings server rendering, fast navigation, and the React ecosystem. The combination works well, but the boundary between them is where most problems appear: authentication that breaks across domains, CORS errors, types that drift apart, and caching that shows stale data. This guide covers the decisions that make the pair pleasant to work with.
When the Split Is Worth It
A separate API and frontend add moving parts, so make sure they buy something. The split pays off when the same API serves a web app and a mobile app, when the frontend needs rich interactivity or strong SEO through server rendering, or when separate teams work on the backend and the frontend. For an internal tool with simple forms and tables, Django templates or the Django admin alone may deliver faster. Many projects use both: Next.js for customer-facing pages and the Django admin for the back office.
Project Structure
- Keep the Django project and the Next.js app in one repository with two folders, so a feature that touches both is one change and one review.
- Split Django into apps by business domain, such as orders, billing, and accounts, rather than by technical layer.
- Version the API under a prefix such as /api/v1/ from the start, so mobile apps that cannot update instantly keep working.
- Use one docker compose file for local development that runs PostgreSQL, Redis, Django, and Next.js together.
Authentication: Keep Tokens out of JavaScript
The most common mistake is storing a JWT in localStorage, where any injected script can read it. Safer options keep the credential in an httpOnly cookie. If the frontend and API share a parent domain, for example app.example.com and api.example.com, Django session authentication with a CSRF token works well and is easy to revoke. If you need JWT, for example because mobile apps use the same API, djangorestframework-simplejwt can issue tokens that the web app receives as httpOnly cookies. Another clean option is to route API calls through Next.js rewrites so the browser sees a single origin, which also removes CORS from the picture.
CORS and CSRF Settings That Work
| Setting | Purpose | Typical value |
|---|---|---|
| CORS_ALLOWED_ORIGINS | Which frontend origins may call the API from the browser | The exact Next.js URLs for each environment, never a wildcard in production |
| CORS_ALLOW_CREDENTIALS | Allows cookies to be sent on cross-origin requests | True when using cookie-based authentication |
| CSRF_TRUSTED_ORIGINS | Which origins may send unsafe requests such as POST with a CSRF token | The same frontend URLs, including the scheme |
| SESSION_COOKIE_SAMESITE and CSRF_COOKIE_SAMESITE | Controls when the browser sends the cookies | Lax for a shared parent domain |
CORS is handled with the django-cors-headers package. When a request fails, check the browser network tab first: the preflight OPTIONS response usually says exactly which header or origin was rejected.
Share Types Through an OpenAPI Schema
Without a contract, the frontend and backend drift apart one renamed field at a time. Generate an OpenAPI schema from the DRF serializers and views with drf-spectacular, then generate TypeScript types or a typed client with a tool such as openapi-typescript. Run the generation in CI and fail the build when the frontend no longer compiles against the latest schema. A renamed field then breaks the build instead of a production page.
Fetching Data in Next.js
- Fetch in Server Components when the data is needed for the first render or for SEO, and forward the user's cookies when the request needs authentication.
- Be explicit about caching for each request: public catalogue data can be cached and revalidated, while user-specific data should not be cached.
- Use a client-side library such as TanStack Query for data that changes while the user is on the page, such as notifications or live status.
- Handle API errors in one place, so an expired session sends the user to login instead of showing a broken page.
Keep the API Fast
Nested serializers make it easy to trigger hundreds of queries for one list endpoint. Use select_related for foreign keys and prefetch_related for many-to-many and reverse relations, and check the query count in tests or with Django Debug Toolbar during development. Paginate every list endpoint. Move slow work, such as sending email, generating PDFs, or calling payment gateways, to background jobs with Celery and Redis, so the API responds quickly and retries happen safely.
Deployment
- 1Build a Docker image for Django that runs gunicorn, or uvicorn if you use async views, with collectstatic done at build time.
- 2Run database migrations as a separate step before the new version receives traffic.
- 3Deploy Next.js as a Node.js server or on a platform that supports it, with the API URL provided as an environment variable per environment.
- 4Put both behind HTTPS with the domains planned for cookies and CORS, and set DEBUG to False and a strict ALLOWED_HOSTS in production.
- 5Add health checks for both services and monitor API latency and error rates.
Set up authentication across both apps in the first week, using the same domains you will use in production. Cookie and CORS problems are cheap to solve on day five and expensive on day ninety.
Key takeaways
- Split Django and Next.js when the API serves several clients, the frontend needs server rendering, or separate teams own each side.
- Keep authentication in httpOnly cookies, using sessions with CSRF or JWT in cookies, never tokens in localStorage.
- Configure CORS and CSRF with exact origins, or route API calls through Next.js rewrites to avoid CORS entirely.
- Generate TypeScript types from a drf-spectacular schema and check them in CI.
- Avoid N plus one queries, paginate lists, and move slow work to Celery.


