Writing/One-person data team
Article · Django · React

From Laptop to Azure: Designing an Enterprise Django + React Stack — Part 1: The Local Foundation

How I designed the local foundation for an enterprise Django and React application so the move from a developer laptop to Azure does not require rebuilding the architecture.

Before writing the first business feature, I spent time designing the infrastructure for a new enterprise level full stack application. The backend is Django, the frontend is React with TypeScript, the database is PostgreSQL, and the cloud destination is Microsoft Azure.

It would have been easy to start with a model, build a screen and connect the two with an API. That gives visible progress very quickly. I decided to do the opposite.

I wanted the local environment to establish the same important boundaries that the application will keep when it moves into a shared development environment and later into production.

Before the first feature

The application will move through three environments.

LOCAL

Developer laptop

  • Frontend React and Vite
  • Backend Django
  • Database PostgreSQL 16
DEV

Azure development

  • Frontend Azure Static Web Apps
  • Backend Azure App Service
  • Database Azure PostgreSQL
PROD

Azure production

  • Frontend Azure Static Web Apps
  • Backend Azure App Service
  • Database Azure PostgreSQL

The local environment does not need to be an exact copy of Azure. React and Django can run directly on my laptop while Azure runs them as managed services.

What matters more is that the main boundaries stay the same. The frontend remains separate from the API. The API owns the database. PostgreSQL is used from the beginning. Configuration comes from the environment, and the frontend and backend can be deployed independently.

Start with the repository boundary

The workspace started with two folders.

Local workspace
project
├── frontend
└── backend

The parent folder is only a workspace. It is not a Git repository.

The frontend and backend each have their own repository.

Git boundary
project
├── frontend
│   └── .git
│
└── backend
    └── .git

I chose this structure because the two applications will eventually be deployed separately.

The React application will go to Azure Static Web Apps. The Django API will go to Azure App Service. They have different dependencies, build processes and release jobs.

Architecture decision

Use two Git repositories

The frontend and backend are separate deployment units, so I keep them as separate source repositories as well. The parent workspace remains a normal folder.

Build the frontend with the current React stack

I started the frontend using the current Vite template for React and TypeScript.

PowerShell
npm create vite@latest . -- --template react-ts --no-interactive
npm install
npm run dev

The development server started at http://localhost:5173.

I then checked the actual versions installed by the current scaffold. I did not want an older planning document to silently decide which versions a new application should use.

Verified frontend environment
Node.js 24.18.0
npm 11.16.0
React 19.2.8
TypeScript 6.0.2
Vite 8.2.2
Oxlint 1.79.0

Some of the first technical notes for the application referenced older versions of React and Vite. I decided not to downgrade a clean modern scaffold just to make it match an old document.

Working rule

Documentation follows the application

The lock file describes what the application actually uses. Documentation should be updated when the implementation changes.

Keep the Django environment isolated

The backend uses Python 3.12. I created a virtual environment inside the backend repository.

PowerShell
python -m venv .venv
.\.venv\Scripts\activate

The virtual environment keeps the Python packages for this application separate from other Python projects on the machine.

The .venv directory is local development infrastructure. It is ignored by Git.

Use PostgreSQL from the first migration

Django starts with SQLite because it makes a new project very easy to run. For a prototype that is useful. For this application I wanted something different.

Both the Azure development environment and the production environment will use PostgreSQL. I wanted the local application to use the same database engine.

Option considered

SQLite locally

Very simple startup, but the database engine changes when the application moves to Azure.

Selected

PostgreSQL locally

A little more setup, but the same database engine is used in local development, Azure development and Azure production.

The difference may not matter when an application has only a few tables and simple queries. It becomes more important as the application grows.

Constraints matter. Indexes matter. Transactions matter. Concurrency matters. JSON behaviour matters. Database specific features matter.

Architecture decision

Use PostgreSQL from day one

If PostgreSQL will be the production database, I would rather find PostgreSQL problems while developing locally than after deployment.

PostgreSQL was the decision. Docker was the tool.

This was one of the most useful distinctions in the setup.

I did not need Docker because the application is intended for enterprise use. I needed PostgreSQL locally. Docker gave me a clean way to run it.

Option considered

Everything in Docker

React, Django and PostgreSQL all run in containers.

Selected

Hybrid local setup

React and Django run directly on Windows. PostgreSQL runs in Docker.

This keeps normal frontend and backend development simple while still giving the application a repeatable database environment.

FRONTEND

Windows host

  • Runtime Node.js
  • Framework React and Vite
  • Port 5173
+
BACKEND

Windows host

  • Runtime Python 3.12
  • Framework Django
  • Port 8000
+
DATABASE

Docker

  • Engine PostgreSQL 16
  • Storage Docker volume
  • Port 5432

Define PostgreSQL with Docker Compose

The database definition lives in backend/compose.yaml.

compose.yaml
services:
  db:
    image: postgres:16
    container_name: app-postgres-local

    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

    ports:
      - "5432:5432"

    volumes:
      - postgres_data:/var/lib/postgresql/data

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

The named volume matters because the container and the database data should not have the same lifetime.

I want the container to be disposable. I do not want the local database to disappear simply because the PostgreSQL container is recreated.

The health check also gives me something better than knowing that the container process started. It checks whether PostgreSQL is ready to accept connections.

Keep real configuration outside Git

The Compose file does not contain the real database password. It reads the values from environment variables.

The backend has two environment files with different jobs.

Backend configuration
backend
├── .env
└── .env.example

.env contains the real local values and is ignored by Git.

.env.example documents the configuration the application expects without storing real secrets.

.env.example
DJANGO_SECRET_KEY=replace-with-local-secret
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1

POSTGRES_DB=app_local
POSTGRES_USER=app_user
POSTGRES_PASSWORD=replace-with-local-password
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
Security rule

Commit the contract, not the secret

The repository can explain which configuration values are required without containing the real credentials.

Start the database and prove that it is healthy

Before starting PostgreSQL, I asked Docker Compose to validate the configuration.

PowerShell
docker compose config --quiet
docker compose up -d
docker compose ps
Result
app-postgres-local
Up
healthy

0.0.0.0:5432 → 5432/tcp
Verified

PostgreSQL 16 was running in Docker and accepting connections on localhost port 5432.

Choose Django 5.2 LTS deliberately

When I first installed the backend packages without restricting the Django version, pip selected the newest available Django release.

It would have worked with the Python version I was using. I still chose not to keep it.

For this application I selected the Django 5.2 LTS line.

Verified Django version
Django 5.2.17

This application is expected to live for years. I prefer a stable support path over choosing a release simply because it has the highest version number.

Architecture decision

Use the LTS release line

Newest and most suitable are not always the same thing. For the first version of this application I prefer the predictable support path of Django 5.2 LTS.

Install the backend foundation before the business code

I installed the packages needed for the application foundation before creating business apps.

PowerShell
pip install django djangorestframework drf-spectacular "psycopg[binary]" django-environ django-q2 ruff pytest pytest-django

Django provides the application framework. Django REST Framework provides the API layer. drf spectacular gives the project an OpenAPI foundation. psycopg connects Django to PostgreSQL.

django environ handles environment based configuration. Django Q2 provides a starting point for background work. Ruff handles Python linting and formatting. Pytest provides the testing foundation.

After installing the packages I checked the Python environment.

PowerShell
python -m pip check
Result
No broken requirements found.

Create the Django project without another folder

I created the Django project directly inside the backend repository.

PowerShell
django-admin startproject config .

The final dot matters. It tells Django to use the current directory instead of creating another nested project folder.

Backend foundation
backend
├── config
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
│
├── compose.yaml
├── manage.py
├── requirements.txt
├── .env
├── .env.example
└── .gitignore

There were still no business apps at this point. I wanted the framework and infrastructure working before adding application behaviour.

Replace SQLite before the first migration

Django generated its normal SQLite configuration. I changed it before running a single migration.

The settings load values through django-environ.

config/settings.py
from pathlib import Path
import environ

BASE_DIR = Path(__file__).resolve().parent.parent

env = environ.Env(
    DEBUG=(bool, False),
)

environ.Env.read_env(BASE_DIR / ".env")

SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env.bool("DEBUG", default=False)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])

The database settings also read from the environment.

config/settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": env("POSTGRES_DB"),
        "USER": env("POSTGRES_USER"),
        "PASSWORD": env("POSTGRES_PASSWORD"),
        "HOST": env("POSTGRES_HOST", default="127.0.0.1"),
        "PORT": env("POSTGRES_PORT", default="5432"),
    }
}

Do not assume the database connection works

A configuration file can look correct while the database is still unreachable.

I asked Django to open a real connection before running migrations.

PowerShell
python manage.py shell -c "from django.db import connection; connection.ensure_connection(); print(connection.vendor); print(connection.settings_dict['NAME'])"
Result
postgresql
app_local
Verified

Django had opened a real PostgreSQL connection. The project was no longer only configured correctly on paper.

Let the database history start on PostgreSQL

I ran Django's system check before creating any tables.

PowerShell
python manage.py check
python manage.py migrate

Django created its authentication, administration, session and content type tables directly in PostgreSQL.

There was no temporary SQLite phase and no db.sqlite3 file to replace later.

I then started the Django development server.

PowerShell
python manage.py runserver
Verified

Django loaded successfully at http://127.0.0.1:8000 and was using PostgreSQL 16.

Give the frontend one local API path

The frontend runs on port 5173. Django runs on port 8000.

I did not want local backend addresses repeated throughout the frontend code. Instead I configured Vite to proxy requests beginning with /api.

vite.config.ts
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],

  server: {
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8000',
        changeOrigin: true,
      },
    },
  },
})

The React application can now use a path such as /api/health. During local development Vite can forward the request to Django.

BROWSER

React

  • Address localhost:5173
  • API path /api
LOCAL PROXY

Vite

  • Receives /api/*
  • Forwards to port 8000
API

Django

  • Address 127.0.0.1:8000
  • Database PostgreSQL

After changing the configuration I also ran a full frontend build.

PowerShell
npm run build
Verified

TypeScript compiled successfully and Vite produced the frontend production build.

Prepare the API contract before building the API

Django REST Framework and drf spectacular are already part of the backend foundation.

Django REST Framework will provide the API. drf spectacular will provide its OpenAPI schema.

Later, the frontend can use that schema to generate TypeScript types. This reduces the chance that the frontend and backend silently develop different ideas of what an API response looks like.

Contract decision

Let the backend own the API contract

Django defines the API. OpenAPI describes it. The frontend can then generate types from that contract instead of relying on manually copied interfaces.

Do not add another service before it is needed

The backend also includes Django Q2 for background work.

For the first version I configured it to use the application's existing database rather than adding Redis immediately.

Redis may make sense later. Right now there is no workload that proves I need it.

Architecture decision

Keep the first local stack small

React, Django and PostgreSQL are enough for the foundation. Additional infrastructure can be introduced when a real requirement justifies it.

Environment rules apply to the frontend too

The frontend uses the same separation between real local configuration and the configuration contract.

Frontend configuration
frontend
├── .env.local
└── .env.example
frontend/.env.example
VITE_API_BASE_URL=/api
VITE_APP_ENV=local
VITE_APP_VERSION=local

There is one important difference on the frontend.

Values exposed through Vite can become part of the browser bundle. Anything beginning with VITE_ should therefore be treated as public configuration.

Database passwords, application secrets and private API credentials do not belong there.

Add quality checks before there is much code

Coding standards are easier to introduce when the codebase is still small.

The backend uses Ruff for Python linting and formatting.

backend/pyproject.toml
[tool.ruff]
target-version = "py312"
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

Ruff is also connected to Git through pre commit.

backend/.pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.16.5
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format

The current Vite scaffold already included Oxlint. I kept it and added Prettier for formatting.

frontend/.pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: oxlint
        name: oxlint
        entry: npm run lint
        language: system
        pass_filenames: false

      - id: prettier
        name: prettier
        entry: npm run format:check
        language: system
        pass_filenames: false
Frontend verification
oxlint.....................................Passed
prettier...................................Passed
Backend verification
ruff check.................................Passed
ruff format................................Passed

Both repositories finished the setup with clean initial commits and clean working trees.

What exists after the first setup session

There is still no business feature.

There is no custom Django model, no real application API and no business screen.

That is intentional.

Local foundation verified
Python 3.12.10
Django 5.2.17
PostgreSQL 16
Docker 29.7.2
Docker Compose 5.5.0
Node.js 24.18.0
React 19.2.8
Vite 8.2.2

The frontend runs.

The backend runs.

Django connects to PostgreSQL 16.

PostgreSQL has persistent local storage.

Secrets stay outside Git.

The frontend has a clean local path to the API.

Both repositories have automatic quality checks.

That is enough for the first foundation.

Local does not need to look exactly like Azure

I am not trying to reproduce Azure on my laptop.

I am trying to keep the important architecture decisions consistent.

Layer Local Azure
Frontend React and Vite on Windows Azure Static Web Apps
Backend Django in a Python virtual environment Azure App Service
Database PostgreSQL 16 in Docker Azure Database for PostgreSQL
Configuration Local environment files Azure application configuration
Source Two local Git repositories Two GitHub repositories

The runtime changes between environments. The basic shape of the application does not.

The local environment is part of the architecture

It is easy to describe all of this as project setup.

I see it as the first architecture work.

Repository boundaries affect deployment. The database choice affects development and migration behaviour. Environment rules affect security. Dependency choices affect how long the application can be maintained. Quality checks affect every commit that follows.

PostgreSQL was the architecture decision. Docker was only the way I chose to run it locally.

None of these decisions produces a useful business feature on the first day.

Their value appears later because they make the next hundred decisions easier.

That is what I wanted from the local foundation.

Build locally in a way that makes the path to Azure boring.

Coming next · Part 2

Structuring the application

The next part will move from infrastructure into the Django and React application structure, API boundaries, health checks and the first real application modules.