Creating a Full Stack Application with Django, FastAPI, and Next.js

How to create a full stack app with Django, FastAPI and Next.js.

Engineering

In this tutorial, we will create a full stack application using Django for the backend, FastAPI for the API, and Next.js for the frontend. This stack is fast, flexible, and has been used on many projects, including those with millions of users at AppSumo.

  • Django: We use Django for its strong scaffolding abilities, primarily for its models, migrations, and admin capabilities.
  • FastAPI: We choose FastAPI because of its speed, OpenAPI support, built-in docs, and Pydantic schemas and validation.
  • Next.js: We utilize Next.js for its flexibility in providing server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR) on a per-page basis.

The source code for this tutorial can be found at https://github.com/damianhodgkiss/next-django-fastapi-fullstack-tutorial/. However, I recommend following the full tutorial to gain a comprehensive understanding of each step.

We choose to use Docker containers for hosting portability. While we may miss out on some instant auto-deployment features like those offered by Vercel, this configuration is designed for flexibility and major cloud providers such as AWS, Azure, and GCP. Additionally, self-hosting on a VPS with Portainer would be fairly straightforward with this stack too.

Prerequisites

Before we begin, ensure that you have the following installed:

  • Python: If not already installed, download and install Python from the official website.
  • django-admin: If not installed, run the following command to install it:
pip install django

Note: Since this is a Docker application, we only need to install enough to set up Django and run create-next-app on the host machine.

Steps

1. Create Project Directory

Create a new directory for the project and navigate into it:

mkdir next-django-fastapi-fullstack
cd next-django-fastapi-fullstack

2. Set Up Django Backend

2.1 Start Django Project

Create a backend directory and start a new Django project:

mkdir backend
django-admin startproject mysaas backend

2.2 Create Dockerfile

Create a Dockerfile in the backend directory with the following content:

FROM python:3.11.5-bullseye 

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1  

RUN pip install --upgrade pip 

COPY . /app/

RUN mkdir -p /app/staticfiles/

RUN pip install -r requirements.txt  

EXPOSE 8000

CMD ["uvicorn", "mysaas.asgi:application", "--host", "0.0.0.0"]

2.3 Configure Python Packages

Edit the requirements.txt file in the backend directory to include the necessary packages:

Django==5.0.3
uvicorn==0.25.0
fastapi==0.109.1
django-use-email-as-username==1.4.0
psycopg2==2.9.9

2.4 Configure Database and Email Authentication

Edit the settings.py file in the backend/mysaas directory to configure the database and email authentication:

ALLOWED_HOSTS = ['localhost']

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.getenv("POSTGRES_DB", default="mysaas"),
        "USER": os.getenv("POSTGRES_USER", default="mysaas"),
        "PASSWORD": os.getenv("POSTGRES_PASSWORD", default="mysaas"),
        "HOST": os.getenv("POSTGRES_HOST", default="postgres"),
        "PORT": os.getenv("POSTGRES_PORT", default="5432"),
    }
}

INSTALLED_APPS = [
    "django_use_email_as_username.apps.DjangoUseEmailAsUsernameConfig",
    ...
]

2.5 Initialize PostgreSQL

Create a postgres directory and an init.sql file within it:

CREATE USER mysaas WITH PASSWORD 'mysaas';
CREATE DATABASE mysaas;
GRANT ALL PRIVILEGES ON DATABASE mysaas TO mysaas;
\connect mysaas;
GRANT CREATE ON SCHEMA public TO mysaas;

Add the following configuration for the PostgreSQL service in the docker-compose.yml file:

postgres:
  image: pgautoupgrade/pgautoupgrade:latest
  restart: always
  volumes:
    - postgres-data:/var/lib/postgresql/data
    - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
  environment:
    POSTGRES_PASSWORD: postgres

volumes:
  postgres-data:

2.6 Run Django

Start the Django application using Docker Compose:

docker compose up --build -d

Check if Django is running by opening http://localhost:8000 in your browser.

2.7 Configure Custom User Model with django-use-email-as-username

Before we configure the database and email authentication, let's set up a custom user model using django-use-email-as-username.

First, create a custom users app:

docker compose exec admin python manage.py create_custom_user_app users

Now, edit the backend/mysaas/settings.py file to use the custom user model:

# Add 'users' to INSTALLED_APPS
INSTALLED_APPS = [
    "users.apps.UsersConfig",  # Add this line
    # ... other installed apps ...
]

# Set the custom user model
AUTH_USER_MODEL = 'users.User'

2.8 Configure Static Files

Now we need to configure static files to ensure the Django admin interface works correctly.

Edit backend/mysaas/settings.py to set the STATIC_ROOT:

# Add this line at the end of the file
STATIC_ROOT = 'static'

Edit backend/mysaas/urls.py to serve static files during development:

from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path("admin/", admin.site.urls),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Collect static files by running the following command:

docker compose exec admin python manage.py collectstatic

This command will gather all static files from your apps and place them in the directory specified by STATIC_ROOT.

2.9 Run Migrations

Run the database migrations:

docker compose exec admin python manage.py migrate

Congratulations! Django is now configured.

3. Set Up FastAPI

3.1 Edit ASGI Configuration

Edit the asgi.py file in the backend/mysaas directory:

"""
ASGI config for mysaas project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application
from fastapi import FastAPI

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysaas.settings")

application = get_asgi_application()
fastapp = FastAPI(
    servers=[
        {
            "url": "/api/v1",
            "description": "V1",
        }
    ]
)


def init(app: FastAPI):
    @app.get("/health")
    def health_check():
        return {'status': 'ok'}


init(fastapp)

3.2 Update Docker Compose Configuration

Add the FastAPI service to the docker-compose.yml file:

api:
  build:
    context: backend
    dockerfile: Dockerfile
  image: backend:latest
  ports:
    - '8001:8000'
  env_file:
    - ./backend/.env
  volumes:
    - ./backend:/app
  command: uvicorn mysaas.asgi:fastapp  --host 0.0.0.0 --reload
  depends_on:
    - postgres

3.3 Run FastAPI

Start the FastAPI service using Docker Compose:

docker compose up --build -d

Check if FastAPI is running by opening http://localhost:8001/docs in your browser.

4. Set Up Next.js Frontend

4.1 Create Next.js App

Create a new Next.js app using the following command:

npx create-next-app@latest frontend --tailwind --typescript --eslint --app --src-dir --import-alias "@/*"

4.2 Create Dockerfile

Create a Dockerfile in the frontend directory:

FROM node:21-bookworm AS base
ARG DEBIAN_FRONTEND=noninteractive

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
# ENV NEXT_TELEMETRY_DISABLED 1

USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
  libc6-dev \
  libvips-dev \
  build-essential \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /app
RUN chown node:node /app


## Install dependencies based on the preferred package manager, and build the app
FROM base AS builder
USER root

# COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
COPY --chown=node:node . .
USER node
RUN \
  if [ -f yarn.lock ]; then yarn config set global-folder /app/.yarn && yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi

RUN yarn build

## Copy the built app to a new image
FROM base AS runner
COPY --from=builder --chown=node:node /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
COPY --from=builder --chown=node:node /app/node_modules/ ./node_modules/

USER node
ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/app/node_modules/.bin
EXPOSE 3000
CMD ["node", "server.js"]

Add the frontend service configuration to the docker-compose.yml file:

frontend:
  build:
    context: frontend
  image: frontend:latest
  ports:
    - '3000:3000'
  volumes:
    - ./frontend:/app
  command: yarn dev
  depends_on:
    - api

4.3 Configure Next.js Output Mode

Change the output mode to standalone in the next.config.mjs file:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
};

export default nextConfig;

4.4 Check Next.js

Verify that Next.js is running by opening http://localhost:3000 in your browser.

5. Configure Nginx

5.1 Create Nginx Configuration

Create an nginx directory and an nginx.conf file within it:

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;
    client_max_body_size 100M;  # allow large file uploads

    keepalive_timeout  65;

    #gzip  on;

    #include /etc/nginx/conf.d/*.conf;

    upstream admin {
      server admin:8000;
    }

    upstream api {
      server api:8000;
    }

    upstream frontend {
      server frontend:3000;
    }

    server {
      listen 80;

      location /admin {
        proxy_pass http://admin;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
      }

      location /static {
          proxy_pass http://admin;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
      }

      location /media {
          proxy_pass http://admin;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
      }

      location /openapi.json {
          proxy_pass http://api;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
      }

      location /docs {
          proxy_pass http://api;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
      }

      location ~ ^/api/v1(/?)(.*) {
          proxy_pass http://api/$2$is_args$args;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
      }

      location / {
        proxy_pass http://frontend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
      }
    }
}

5.2 Add Nginx to Docker Compose

Add the Nginx service to the docker-compose.yml file:

nginx:
  image: nginx
  ports:
    - "80:80"
  volumes:
    - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
  depends_on:
    - frontend
    - admin
    - api

5.3 Start Nginx

Start Nginx using Docker Compose:

docker compose up --build -d

Check the following URLs to verify that everything is working correctly:

Conclusion

Congratulations! You have successfully created a full stack application using Django, FastAPI, and Next.js. This tutorial provided a step-by-step guide on setting up and configuring each component to work together seamlessly.

I will be providing more tutorials in the future that will use this starter as the basis for extending the application further.