By KFSys
System Administrator
Modern web applications often separate the backend API from the frontend interface. Django provides a robust backend with Django REST Framework for building APIs, while Next.js offers server-side rendering, static generation, and an optimized developer experience for the frontend.
In this tutorial, you will build a small event listing application with a Django REST API backend and a Next.js frontend, then deploy both components to DigitalOcean’s App Platform — a fully managed Platform-as-a-Service (PaaS) that handles infrastructure, SSL, and scaling for you.
By the end of this tutorial, you will have:
Before you begin, you will need:
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Accepted Answer
First, create a project directory and set up the Django backend.
Create the project structure:
mkdir django-nextjs-app && cd django-nextjs-app
mkdir backend && cd backend
Create and activate a Python virtual environment:
python3 -m venv venv
source venv/bin/activate
Install Django and the required packages:
pip install django djangorestframework django-cors-headers gunicorn psycopg2-binary dj-database-url python-decouple whitenoise
Create a new Django project and app:
django-admin startproject config .
python manage.py startapp events
Your backend/ directory should now look like this:
backend/
├── config/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── events/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── views.py
│ └── ...
├── manage.py
└── venv/
Open backend/config/settings.py and update it to support environment variables and production deployment:
# backend/config/settings.py
import os
from pathlib import Path
from decouple import config, Csv
import dj_database_url
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = config('DJANGO_SECRET_KEY', default='change-me-in-production')
DEBUG = config('DJANGO_DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('DJANGO_ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=Csv())
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party
'rest_framework',
'corsheaders',
# Local
'events',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database
DATABASES = {
'default': dj_database_url.config(
default=config('DATABASE_URL', default=f'sqlite:///{BASE_DIR / "db.sqlite3"}'),
conn_max_age=600,
)
}
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# CORS
CORS_ALLOWED_ORIGINS = config(
'CORS_ALLOWED_ORIGINS',
default='http://localhost:3000',
cast=Csv()
)
# REST Framework
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
}
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
This configuration uses python-decouple to read environment variables, dj-database-url to parse the database connection string, and whitenoise to serve static files efficiently in production.
Define the Event model in backend/events/models.py:
# backend/events/models.py
from django.db import models
class Event(models.Model):
title = models.CharField(max_length=200)
description = models.TextField()
date = models.DateTimeField()
location = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['date']
def __str__(self):
return self.title
Create a serializer in backend/events/serializers.py:
# backend/events/serializers.py
from rest_framework import serializers
from .models import Event
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ['id', 'title', 'description', 'date', 'location', 'created_at']
Create the API view in backend/events/views.py:
# backend/events/views.py
from rest_framework import generics
from .models import Event
from .serializers import EventSerializer
class EventListView(generics.ListAPIView):
queryset = Event.objects.all()
serializer_class = EventSerializer
Wire up the URLs in backend/events/urls.py:
# backend/events/urls.py
from django.urls import path
from .views import EventListView
urlpatterns = [
path('events/', EventListView.as_view(), name='event-list'),
]
Include the events URLs in backend/config/urls.py:
# backend/config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('events.urls')),
]
Run the migrations and create some sample data:
python manage.py makemigrations events
python manage.py migrate
python manage.py createsuperuser
Start the development server and add a few events through the admin at http://localhost:8000/admin/:
python manage.py runserver
Verify the API works by visiting http://localhost:8000/api/events/ in your browser.
Create a backend/requirements.txt file:
pip freeze > requirements.txt
Create a backend/runtime.txt to specify the Python version:
python-3.11.7
Create a backend/.env.example for reference (do not commit your actual .env file):
DJANGO_SECRET_KEY=your-secret-key
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=.ondigitalocean.app
DATABASE_URL=postgresql://user:password@host:port/dbname
CORS_ALLOWED_ORIGINS=https://your-frontend.ondigitalocean.app
Navigate back to the root directory and create the Next.js app:
cd ..
npx create-next-app@latest frontend --typescript --tailwind --app --eslint --src-dir --no-import-alias
cd frontend
Create an API utility to fetch data from the Django backend. Create frontend/src/lib/api.ts:
// frontend/src/lib/api.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
export interface Event {
id: number;
title: string;
description: string;
date: string;
location: string;
created_at: string;
}
export async function getEvents(): Promise<Event[]> {
const res = await fetch(`${API_BASE_URL}/api/events/`, {
next: { revalidate: 60 },
});
if (!res.ok) {
throw new Error('Failed to fetch events');
}
return res.json();
}
Replace the contents of frontend/src/app/page.tsx with the events listing page:
// frontend/src/app/page.tsx
import { getEvents } from '@/lib/api';
export default async function Home() {
const events = await getEvents();
return (
<main className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
Upcoming Events
</h1>
<p className="text-gray-600 mb-8">
Browse the latest events happening near you.
</p>
{events.length === 0 ? (
<p className="text-gray-500 text-center py-12">
No events found. Check back later!
</p>
) : (
<div className="grid gap-6">
{events.map((event) => (
<article
key={event.id}
className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
>
<div className="flex justify-between items-start">
<div>
<h2 className="text-xl font-semibold text-gray-900">
{event.title}
</h2>
<p className="text-gray-600 mt-2">{event.description}</p>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-4 text-sm text-gray-500">
<span className="flex items-center gap-1">
📅{' '}
{new Date(event.date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</span>
<span className="flex items-center gap-1">
📍 {event.location}
</span>
</div>
</article>
))}
</div>
)}
</div>
</main>
);
}
Update frontend/src/app/layout.tsx:
// frontend/src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Event Listings — Django + Next.js on DigitalOcean',
description: 'A demo event listing app built with Django and Next.js, deployed on DigitalOcean App Platform.',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
Test the frontend locally (make sure your Django server is running on port 8000):
npm run dev
Visit http://localhost:3000 to confirm events render correctly.
DigitalOcean App Platform needs Next.js to run in standalone mode to minimize the deployed bundle size. Update frontend/next.config.ts:
// frontend/next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone',
};
export default nextConfig;
Initialize a Git repository from the project root:
cd .. # back to django-nextjs-app/
git init
Create a .gitignore in the root:
# Python
backend/venv/
backend/__pycache__/
backend/**/__pycache__/
backend/db.sqlite3
backend/.env
*.pyc
# Node
frontend/node_modules/
frontend/.next/
frontend/.env.local
# OS
.DS_Store
Commit and push:
git add .
git commit -m "Initial commit: Django + Next.js event app"
git remote add origin https://github.com/your-username/django-nextjs-app.git
git branch -M main
git push -u origin main
Replace your-username with your actual GitHub username.
Before deploying the app, set up a managed database:
events-dbOnce provisioning completes (typically 3–5 minutes):
host, port, username, password, and databaseNow deploy both the Django backend and Next.js frontend as components of a single App Platform application.
django-nextjs-app repository and the main branchClick Edit on the detected component, or Add Resource → Service
Set the following:
backend/backendpip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrategunicorn config.wsgi --bind 0.0.0.0:80808080Under Environment Variables, add:
| Variable | Value |
|---|---|
DJANGO_SECRET_KEY |
(click “Generate” or paste a strong random string) |
DJANGO_DEBUG |
False |
DJANGO_ALLOWED_HOSTS |
.ondigitalocean.app |
DATABASE_URL |
(paste the connection string from Step 8) |
CORS_ALLOWED_ORIGINS |
(leave blank for now — you’ll update this after the frontend deploys) |
Tip: Mark
DJANGO_SECRET_KEYandDATABASE_URLas Encrypted to keep them secure.
Click Add Resource → Service
Set the following:
frontend/frontendnpm ci && npm run buildnode .next/standalone/server.js3000Under Environment Variables, add:
| Variable | Value |
|---|---|
NEXT_PUBLIC_API_URL |
https://backend-xxxxx.ondigitalocean.app (use the backend’s URL from App Platform) |
PORT |
3000 |
events-db cluster from Step 8DATABASE_URL environment variableClick Create Resources. DigitalOcean will now:
The first deployment takes approximately 5–10 minutes.
Once both components are live, you need to update the backend’s CORS configuration with the frontend’s actual URL.
CORS_ALLOWED_ORIGINS to your frontend URL, for example:
https://frontend-xxxxx.ondigitalocean.app
After the redeployment finishes, visit your frontend URL. You should see your events listed, rendered server-side by Next.js and served from your Django API.
DigitalOcean App Platform supports automatic deployments out of the box. Every time you push to the main branch, both components rebuild and redeploy automatically.
To verify this:
# Make a small change, then push
git add .
git commit -m "Update event listing styles"
git push origin main
Within minutes, your live application reflects the changes — no manual intervention required.
To serve your app on a custom domain:
Remember to update DJANGO_ALLOWED_HOSTS and CORS_ALLOWED_ORIGINS with your custom domain as well.
This approach of pairing Django with Next.js on a managed platform is well-suited for content-driven and event-driven applications — it’s a similar architecture behind production sites like extratime.world, which serves sports event data to users worldwide.
In this tutorial, you built a full-stack application with a Django REST Framework backend and a Next.js frontend, then deployed both to DigitalOcean App Platform with a managed PostgreSQL database.
Your deployed application now has:
From here, you can extend the application by adding user authentication with Django’s auth system and Next.js middleware, implementing real-time updates with WebSockets via Django Channels, or adding an admin interface to manage events through the Django admin panel.
Great write up, thanks for sharing this!
Quick question: if you wanted to add Celery for background tasks (for example sending emails, processing uploads, or scheduled jobs), how would you integrate that into this setup on App Platform? Would you run Celery as a separate worker service in the same app or handle it differently?
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.