Create a Django and React app with shared, auto-generated types

August 18, 2025

Building a Type-Safe Django + React Stack

In this, we'll create a full Django and React app, complete with automatically generated end-to-end type safety from Python models to TypeScript components.

Why This Stack is useful

The type system ensures that any changes to your Python models automatically propagate to your TypeScript frontend when you regenerate the types, providing a robust, type-safe development experience. This means:

  • Zero runtime type errors due to frontend and backend differences
  • Automatic code completion in your IDE for API responses
  • Compile-time guarantees that your frontend matches your backend schema
  • Refactoring confidence - change a model field and TypeScript will tell you everywhere that needs updating

How our Type Propagation works

  1. Define your data model in Django
  2. Create Pydantic schemas that match your models
  3. Django Ninja generates OpenAPI from your schemas automatically
  4. openapi-typescript converts the OpenAPI spec to TypeScript types
  5. Your React components get full autocomplete and type checking

The Complete Tech Stack

CategoryTechnologyWhy
BackendDjango 5 + Django NinjaType-safe REST API with auto-generated docs
FrontendReact + TypeScript + ViteFast development with full type safety
Type Generationopenapi-typescriptConverts OpenAPI to TypeScript types
Data FetchingNative React hooksSimple async state management
ValidationPydanticRuntime validation with static type inference
Type Checkingmypy + TypeScriptStrict type checking on both ends
Package Managementuv (Python) + pnpm (Node)Fast, reliable dependency management
Code Qualityruff + ESLintConsistent formatting and linting

1) Backend Setup with Django + Ninja

Create the Project Environment

# Create and activate virtual environment
uv venv .venv
source .venv/bin/activate
 
# Install dependencies with uv (faster than pip)
uv pip install django django-ninja pydantic mypy "django-stubs[compatible-mypy]" ruff django-cors-headers
 
# Create Django project
django-admin startproject backend .

Create the API App

python manage.py startapp api

Configure Django Settings

Create backend/settings.py :

from pathlib import Path
 
BASE_DIR = Path(__file__).resolve().parent.parent
 
SECRET_KEY = "dev-only"
DEBUG = True
ALLOWED_HOSTS = ["*"]
 
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "corsheaders",
    "api",  # Our API app
]
 
MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "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 = "backend.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 = "backend.wsgi.application"
 
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3"
    }
}
 
STATIC_URL = "static/"
 
# CORS for local Vite development
CORS_ALLOW_ALL_ORIGINS = True

Define Your Models

Create api/models.py :

from django.db import models
 
class Book(models.Model):
    title = models.CharField(max_length=200)
    year = models.PositiveIntegerField()
    author = models.CharField(max_length=120)
 
    def __str__(self) -> str:
        return f"{self.title} ({self.year})"

Create Auto-Generated Schemas

Create api/schemas.py :

from ninja import ModelSchema
from .models import Book
 
class BookIn(ModelSchema):
    class Meta:
        model = Book
        exclude = ['id']
 
class BookOut(ModelSchema):
    class Meta:
        model = Book
        fields = '__all__'

Build Type-Safe API Views

Create api/views.py :

from django.db.models import QuerySet
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from ninja import NinjaAPI
 
from .models import Book
from .schemas import BookIn, BookOut
 
api = NinjaAPI(title="Sample API for a library")
 
@api.get("/books", response=list[BookOut])
def list_books(request: HttpRequest) -> QuerySet[Book]:
    return Book.objects.all().order_by("id")
 
@api.post("/books", response=BookOut)
def create_book(request: HttpRequest, payload: BookIn) -> Book:
    return Book.objects.create(**payload.model_dump())
 
@api.get("/books/{book_id}", response=BookOut)
def get_book(request: HttpRequest, book_id: int) -> Book:
    return get_object_or_404(Book, pk=book_id)

Wire Up URLs

Create backend/urls.py :

from django.contrib import admin
from django.urls import path
from api.views import api
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", api.urls),
]

Run Migrations and Start Server

python manage.py makemigrations
python manage.py migrate
python manage.py runserver

Your API is now live:


2) Type Checking Configuration

Setup mypy for Django

Create mypy.ini in your root folder:

[mypy]
python_version = 3.12
plugins = mypy_django_plugin.main
strict = True
ignore_missing_imports = False
warn_unused_ignores = True
warn_redundant_casts = True
warn_return_any = True
disallow_any_generics = True
no_implicit_optional = True
 
[mypy.plugins.django-stubs]
django_settings_module = backend.settings
 
[mypy-*.migrations.*]
ignore_errors = True

Setup ruff for Code Quality

Create ruff.toml in your root folder:

target-version = "py312"
line-length = 100
 
[lint]
select = ["E","F","I","UP","B"]
 
[lint.per-file-ignores]
"*/migrations/*" = ["E501"]

Verify Type Safety

# Run type checking
uv run mypy api backend
 
# Run linting
uv run ruff check . --fix

3) Frontend Setup with React + TypeScript

Create React App with Vite

# Use pnpm for faster package management
pnpm create vite@latest frontend -- --template react-ts
cd frontend
pnpm install
 
# Install type generation and API client dependencies
pnpm add -D openapi-typescript
pnpm add openapi-fetch

Generate TypeScript Types from OpenAPI

This is where the magic happens.

# Generate types from your Django API
pnpm exec openapi-typescript http://127.0.0.1:8000/api/openapi.json --output src/lib/api-types.ts

This creates fully-typed interfaces that match your Pydantic schemas exactly:

Open api-types.ts to see:

// Generated automatically from your Django API
export interface BookOut {
  id: number
  title: string
  year: number
  author: string
}
 
export interface BookIn {
  title: string
  year: number
  author: string
}

Create a Typed API Client

In order to use our types when fetching data from the backend, we will add a typed client to requests to the backend.

Create src/lib/client.ts :

import createClient from 'openapi-fetch'
import type { paths } from './api-types'
 
export const api = createClient<paths>({
  baseUrl: 'http://127.0.0.1:8000',
})

That's it! Just 3 lines for a fully typed client. The openapi-fetch library handles all the complex type extraction automatically.

Setup React App

Update src/main.tsx :

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

Build Type-Safe React Components

Update src/App.tsx :

import { useCallback, useEffect, useState } from "react";
import type { components } from "./lib/api-types";
import { api } from "./lib/client";
 
type Book = components["schemas"]["BookOut"];
 
export default function App() {
	const [books, setBooks] = useState<Book[]>([]);
	const [loading, setLoading] = useState(true);
	const [error, setError] = useState<string | null>(null);
 
	const loadBooks = useCallback(async () => {
		try {
			const { data } = await api.GET("/api/books");
			setBooks(data || []);
			setError(null);
		} catch (err) {
			setError(err instanceof Error ? err.message : "Failed to load books");
		} finally {
			setLoading(false);
		}
	}, []);
 
	const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
		e.preventDefault();
		const formData = new FormData(e.currentTarget);
		const title = String(formData.get("title") || "");
		const author = String(formData.get("author") || "");
		const year = Number(formData.get("year") || 0);
 
		try {
			await api.POST("/api/books", {
				body: { title, author, year },
			});
			e.currentTarget?.reset();
			loadBooks(); // Refresh the list
		} catch (err) {
			setError(err instanceof Error ? err.message : "Failed to create book");
		}
	};
 
	useEffect(() => {
		loadBooks();
	}, [loadBooks]);
 
	if (loading) return <p>Loading...</p>;
	if (error) return <p>Error: {error}</p>;
 
	return (
		<div style={{ padding: 24 }}>
			<h1>Books</h1>
 
			<form onSubmit={handleSubmit}>
				<input name="title" placeholder="Title" required />
				<input name="author" placeholder="Author" required />
				<input name="year" type="number" placeholder="Year" required />
				<button type="submit">Add</button>
			</form>
 
			<ul>
				{books.map((book) => (
					<li key={book.id || 0}>
						{book.title} by {book.author} ({book.year})
					</li>
				))}
			</ul>
		</div>
	);
}

Start the Frontend

pnpm run dev

Your frontend is now running with full type safety:

**** CONT here. FInish testing then recrdo.

4) Testing & Validation

Add Django Tests

Create api/tests.py :

from django.test import TestCase
 
class ApiTest(TestCase):
    def test_list_and_create(self) -> None:
        r = self.client.post(
            "/api/books",
            data={"title": "Dune", "year": 1965, "author": "Herbert"},
            content_type="application/json",
        )
        self.assertEqual(r.status_code, 200)
        self.assertEqual(r.json()["title"], "Dune")
 
        r2 = self.client.get("/api/books")
        self.assertEqual(r2.status_code, 200)
        self.assertEqual(len(r2.json()), 1)

Run All Checks

# Test your Django API
python manage.py test
 
# Type check your Python code
mypy api backend
 
# Lint your Python code
ruff check .
 
# Build your frontend (includes TypeScript checking)
cd frontend && pnpm run build

5) The Type Safety Magic

Example: Adding a New Field

Let's add a description field to our Book model:

Step 1: Update the Django model

class Book(models.Model):
    title = models.CharField(max_length=200)
    year = models.PositiveIntegerField()
    author = models.CharField(max_length=120)
    description = models.TextField(blank=True)  # New field!

Step 2: Schemas automatically update

The ModelSchema automatically picks up the new field - no manual schema updates needed!

Step 2: Regenerate TypeScript types

pnpm exec openapi-typescript http://127.0.0.1:8000/api/openapi.json --output src/lib/api-types.ts

Step 3: TypeScript now knows about the new field! Your IDE will autocomplete description and TypeScript will enforce that you handle it properly.

Automation Script

Create a script to automate type generation:

#!/bin/bash
# scripts/update-types.sh
cd frontend
pnpm exec openapi-typescript http://127.0.0.1:8000/api/openapi.json --output src/lib/api-types.ts
echo "TypeScript types updated!"

6) Production Considerations

Security

  • Replace CORS_ALLOW_ALL_ORIGINS = True with specific origins
  • Use environment variables for secrets
  • Add authentication/authorization as needed

Automated Type Generation

Add this to your deployment script or pre-commit hook:

# Automate type generation on deploy or precommit
python manage.py runserver --noreload &
SERVER_PID=$!
sleep 5
cd frontend && pnpm exec openapi-typescript http://127.0.0.1:8000/api/openapi.json --output src/lib/api-types.ts
kill $SERVER_PID

That's it!

As mentioned in my article, having a reliable development experience with strong typing makes a huge difference in productivity and confidence when shipping features.

Happy building.

Want to ship better features with AI?
Join my free weekly newsletter.

No spam guaranteed Unsubscribe whenever