Django 5 + PostgreSQL REST API, end to end
Build a notes API with Django 5 and Django REST Framework, use Django's built-in PostgreSQL connection pool, serve static files with WhiteNoise, and deploy it to Deployxa behind Gunicorn with migrations on every release.
What you will build
- A
Notemodel with a CRUD API at/api/notes/, plus the browsable API and the Django admin. - Settings driven entirely by environment variables, so the same code runs locally and on Deployxa.
- Connection pooling with psycopg 3 (Django 5.1+), sized for Gunicorn workers.
You need Python 3.12, a Deployxa account and optionally the CLI (npm install -g @deployxa/cli).
1. Create the project
$ mkdir notes-api && cd notes-api $ python -m venv .venv && source .venv/bin/activate $ pip install "django>=5.1,<6" djangorestframework gunicorn whitenoise dj-database-url "psycopg[binary,pool]" $ django-admin startproject config . $ python manage.py startapp notes $ pip freeze > requirements.txt
The trailing . puts manage.py at the repository root, which is where Deployxa looks for it. The project package is config, so the WSGI module is config.wsgi.
2. The model
# notes/models.py
from django.db import models
class Note(models.Model):
title = models.CharField(max_length=200)
body = models.TextField(blank=True)
pinned = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-pinned", "-updated_at"]
def __str__(self):
return self.title# notes/admin.py from django.contrib import admin from .models import Note admin.site.register(Note)
3. Serializer, viewset and URLs
# notes/serializers.py
from rest_framework import serializers
from .models import Note
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
fields = ["id", "title", "body", "pinned", "created_at", "updated_at"]
read_only_fields = ["created_at", "updated_at"]# notes/views.py
from django.db import connection
from django.http import JsonResponse
from rest_framework import viewsets
from .models import Note
from .serializers import NoteSerializer
class NoteViewSet(viewsets.ModelViewSet):
queryset = Note.objects.all()
serializer_class = NoteSerializer
def health(request):
with connection.cursor() as cur: # proves the database is reachable
cur.execute("SELECT 1")
return JsonResponse({"ok": True})# config/urls.py
from django.contrib import admin
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from notes.views import NoteViewSet, health
router = DefaultRouter()
router.register("notes", NoteViewSet)
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include(router.urls)),
path("health/", health),
]4. Production-ready settings
Replace the matching parts of config/settings.py:
# config/settings.py
import os
from pathlib import Path
import dj_database_url
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv("SECRET_KEY", "dev-only-not-secret")
DEBUG = os.getenv("DEBUG", "0") == "1"
ALLOWED_HOSTS = [h for h in os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") if h]
CSRF_TRUSTED_ORIGINS = [o for o in os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",") if o]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = CSRF_COOKIE_SECURE = not DEBUG
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"notes",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"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",
]
# DATABASE_URL is injected by Deployxa; without it, fall back to SQLite for local work
DATABASES = {
"default": dj_database_url.config(default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}")
}
if DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql":
# Django 5.1+ connection pool (psycopg 3). CONN_MAX_AGE must stay 0 with a pool.
DATABASES["default"]["OPTIONS"] = {"pool": {"min_size": 1, "max_size": 4}}
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static"
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticatedOrReadOnly"],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}max_size 4, the app can open up to 12 connections, so keep workers × max_size below your database plan's connection limit.5. Run it locally
$ python manage.py makemigrations notes $ python manage.py migrate $ python manage.py createsuperuser $ python manage.py runserver # http://127.0.0.1:8000/api/notes/ (log in at /admin/ to create notes)
Commit the generated notes/migrations/0001_initial.py; production only applies migrations, it never creates them. Add a .gitignore with .venv/, db.sqlite3, static/ and __pycache__/.
6. Deploy to Deployxa
- 1Import
Push to GitHub, open
/dashboard/projects/newand pick the repository. - 2Configure
Choose the Django preset, set Database to PostgreSQL, and change the build command so static files are collected:
shellpip install -r requirements.txt && python manage.py collectstatic --noinput
- 3Environment
Add these variables (replace the host with your project's Deployxa URL):
envSECRET_KEY=<output of get_random_secret_key()> ALLOWED_HOSTS=notes-api.deployxa.online CSRF_TRUSTED_ORIGINS=https://notes-api.deployxa.online DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_EMAIL=you@example.com DJANGO_SUPERUSER_PASSWORD=<a strong password>
- 4Start command and port
After the first deploy, open the Environment Canvas, select the web service, set Internal Port to
8000and the start command to:shellsh -c 'python manage.py migrate --noinput && (python manage.py createsuperuser --noinput || true) && gunicorn config.wsgi --bind 0.0.0.0:8000 --workers 3 --access-logfile -'
createsuperuser --noinputreads theDJANGO_SUPERUSER_*variables;|| trueignores the "already exists" error on later deploys. Once the admin account exists you can remove that part and the password variable.
Or from the CLI, after creating the project and attaching PostgreSQL in the dashboard:
$ rm -rf .venv static db.sqlite3 # deploy uploads the folder as-is $ deployxa link $ deployxa env set SECRET_KEY "$(python -c 'from django.core.management.utils import get_random_secret_key as g; print(g())')" $ deployxa env set ALLOWED_HOSTS notes-api.deployxa.online $ deployxa deploy $ deployxa logs --live
7. Try the API
$ curl https://notes-api.deployxa.online/health/
{"ok": true}
$ curl -u admin:<password> -X POST https://notes-api.deployxa.online/api/notes/ \
-H "Content-Type: application/json" \
-d '{"title": "Ship it", "body": "Deployed on Deployxa", "pinned": true}'
$ curl https://notes-api.deployxa.online/api/notes/The admin at /admin/ is styled, which confirms WhiteNoise is serving the collected static files. Basic auth works with the browsable API's defaults; for real clients add token or JWT authentication.
8. Custom domain
Connect api.example.com in Dashboard → Domains (or deployxa domains add api.example.com), create the CNAME record shown, then add the domain to both variables and redeploy:
$ deployxa env set ALLOWED_HOSTS "notes-api.deployxa.online,api.example.com" $ deployxa env set CSRF_TRUSTED_ORIGINS "https://notes-api.deployxa.online,https://api.example.com" $ deployxa redeploy
Troubleshooting
More settings and errors are in Deploy Django.