All articles

Building a Web App with Django REST Framework and Next.js

How to combine a Django REST Framework backend with a Next.js frontend: when the split is worth it, project structure, authentication with cookies, CORS and CSRF, shared types from an OpenAPI schema, server-side data fetching, background jobs, and deployment.

Web Development|Published |10 min read
Source code on a laptop screen in a dark room

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

SettingPurposeTypical value
CORS_ALLOWED_ORIGINSWhich frontend origins may call the API from the browserThe exact Next.js URLs for each environment, never a wildcard in production
CORS_ALLOW_CREDENTIALSAllows cookies to be sent on cross-origin requestsTrue when using cookie-based authentication
CSRF_TRUSTED_ORIGINSWhich origins may send unsafe requests such as POST with a CSRF tokenThe same frontend URLs, including the scheme
SESSION_COOKIE_SAMESITE and CSRF_COOKIE_SAMESITEControls when the browser sends the cookiesLax 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

  1. 1Build a Docker image for Django that runs gunicorn, or uvicorn if you use async views, with collectstatic done at build time.
  2. 2Run database migrations as a separate step before the new version receives traffic.
  3. 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.
  4. 4Put both behind HTTPS with the domains planned for cookies and CORS, and set DEBUG to False and a strict ALLOWED_HOSTS in production.
  5. 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.

Related articles

More articles on software development, AI, cloud, and infrastructure.

A desktop screen showing landing page designs next to a tablet and a phone
Web Development|

How to Build a Company Profile Website That Brings in Leads

What a company profile website needs to bring in enquiries: clear service pages, proof such as case studies and client logos, easy contact options, fast loading on mobile, SEO basics, the right platform, and tracking that shows which pages produce leads.

A customer paying with a phone at a shop counter
Software Development|

Integrating a Payment Gateway Like Midtrans or Xendit Safely

How to integrate an Indonesian payment gateway such as Midtrans or Xendit: choosing payment methods, hosted checkout versus direct API, verifying webhooks, handling duplicate notifications, order status design, expiry, refunds, testing in sandbox, and daily reconciliation.

Looking for a software development partner?

Tell us about your project, what you need to build, and the challenges you are facing. We can discuss the technical approach, scope, timeline, and estimated cost.

Start a conversation