Tutorials · 3 min read · 10 October 2026
Next.js frontend with a Django REST backend
The setup I use for BookMeDrive: Django REST Framework for the API, Next.js for the site, and the three settings that trip everyone up.
BookMeDrive runs on a Django REST API with a Next.js frontend. It's a good split: Django gives you the admin, the ORM and auth out of the box, and Next.js gives you fast pages that Google can read. Getting the two to talk takes about three settings. Here they are.
1. Expose an API from Django
Install Django REST Framework and django-cors-headers:
pip install djangorestframework django-cors-headersThen add them to settings. The CORS middleware needs to sit as high up the list as possible.
INSTALLED_APPS = [
# ...
"rest_framework",
"corsheaders",
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
# ... the rest
]
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"https://yourdomain.com",
]A simple read-only endpoint:
# listings/views.py
from rest_framework import generics
from .models import Listing
from .serializers import ListingSerializer
class ListingList(generics.ListAPIView):
queryset = Listing.objects.filter(is_active=True)
serializer_class = ListingSerializer2. Keep the API address in an environment variable
In the Next.js project, create .env.local:
API_URL=http://localhost:8000
NEXT_PUBLIC_API_URL=http://localhost:8000The difference matters. API_URL is only available on the server, which is where most of your data fetching should happen. Anything starting with NEXT_PUBLIC_ is baked into the browser bundle, so only use it for calls the browser has to make itself, and never put a secret in it.
3. Fetch on the server
In the App Router, a page can fetch data directly. Doing it on the server means the HTML arrives with the listings already in it, which is good for speed and for SEO.
// app/listings/page.tsx
type Listing = { id: number; title: string; city: string };
async function getListings(): Promise<Listing[]> {
const res = await fetch(`${process.env.API_URL}/api/listings/`, {
next: { revalidate: 60 }, // refresh at most once a minute
});
if (!res.ok) throw new Error("Couldn't load listings");
return res.json();
}
export default async function ListingsPage() {
const listings = await getListings();
return (
<ul>
{listings.map((l) => (
<li key={l.id}>
{l.title}, {l.city}
</li>
))}
</ul>
);
}revalidate: 60 means the page is cached and rebuilt in the background at most once a minute, so Django isn't hit on every visit.
Skipping CORS with a rewrite
If you'd rather the browser never talks to Django directly, proxy it through Next.js. The browser calls /api/... on your own domain and Next.js forwards it, so there's no cross-origin request at all.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async rewrites() {
return [{ source: "/api/:path*", destination: `${process.env.API_URL}/api/:path*/` }];
},
};
export default nextConfig;Django adds a trailing slash to its URLs by default, which is why the destination ends with one.
Deploying the pair
- Run Django behind gunicorn and a reverse proxy, with
ALLOWED_HOSTSset to the API domain. - Set
API_URLon your Next.js host to the production API address. - Add the production site to
CORS_ALLOWED_ORIGINSif the browser calls the API directly. - If you use cookies for auth across the two domains, also look at
CSRF_TRUSTED_ORIGINSandCORS_ALLOW_CREDENTIALS.
