Skip to main content

🎸 Django Framework Architecture

Django is the "batteries-included" Python web framework that took Instagram from a photo-sharing app to a billion-user platform. Before you write a single model, it pays to understand the machine you're driving — how a request flows from the browser, through middleware and URL routing, into a view, and back out as a rendered response.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain Django's Model-View-Template (MVT) pattern and how it differs from classic MVC
  • Trace an HTTP request through the full request-response cycle, including middleware
  • Distinguish a Django project from an app and lay out a maintainable structure
  • Configure settings safely for multiple environments (dev, staging, production)
  • Decide when Django is the right tool versus Flask or FastAPI

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Scaffold a real Django 5 project and trace a request from URL to response.

In This Lesson

What Is Django?

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. It was built in 2003–2005 inside the newsroom of the Lawrence Journal-World, where developers needed to ship database-backed features on deadline — and it was named after jazz guitarist Django Reinhardt. That origin story explains its whole personality: opinionated, complete, and fast to build with.

Django's defining trait is that it is "batteries-included." Where a micro-framework hands you a bare request/response loop and asks you to assemble the rest, Django ships with an ORM, an authentication system, an auto-generated admin site, form handling, security middleware, and a template engine — all in the box, all designed to work together.

📖 Key Terms

Framework: a structured foundation of reusable code that dictates the shape of your application, so you fill in the gaps rather than build from scratch.

Batteries-included: the philosophy that common needs (auth, admin, ORM, security) should be provided out of the box.

ORM: Object-Relational Mapper — lets you query the database with Python objects instead of raw SQL.

🎸 The orchestra analogy: Think of Django as a well-rehearsed orchestra. The ORM section handles data, templates play the visuals, the URL dispatcher conducts routing, and the security middleware keeps everyone in time. You don't hire each musician separately — the ensemble arrives ready to perform, and your job is to choose the piece.

What makes Django worth learning:

  • Security by default — protection against SQL injection, XSS, CSRF, and clickjacking is built in.
  • Proven scale — Instagram, Spotify, Pinterest, and Disqus all run Django in production.
  • The admin site — a full data-management interface generated from your models, free.
  • Excellent documentation — widely regarded as some of the best in open source.

The MVT Architecture

Django organizes code with a pattern it calls Model-View-Template (MVT). It is a close cousin of the classic Model-View-Controller (MVC) pattern, but the names are shuffled in a way that trips up newcomers coming from Rails or Laravel.

MVT termResponsibilityClassic MVC equivalent
ModelData structure and database accessModel
ViewRequest handling and business logicController
TemplatePresentation — rendering HTMLView
URLconfMaps URLs to views (the router)Routing layer

⚠️ The naming gotcha

In Django, a "view" is the controller — the Python code that processes a request. The "template" is what actually renders the HTML the user sees. Django's own docs describe the framework as "MTV" and joke that the controller is "the framework itself." Once you internalize this, everything else falls into place.

Here is how the four pieces cooperate to serve a single page — say, a blog post at /blog/post/123/:

graph TD A[Client Browser] -->|HTTP Request| B[URLconf / Routing] B -->|matched pattern| C[View] C -->|query| D[Model] D -->|SQL| E[(Database)] E -->|rows| D D -->|Python objects| C C -->|context data| F[Template] F -->|rendered HTML| A

The MVT flow: the URLconf routes to a view, the view talks to models, and a template turns the data into HTML.

💡 Walkthrough: serving one blog post

  1. A user requests example.com/blog/post/123/.
  2. The URLconf matches the pattern and calls the matching view.
  3. The view uses a model to fetch post #123 from the database.
  4. The view hands the post data to a template as "context".
  5. The template renders HTML, and Django returns it to the browser.

Django vs. Flask vs. FastAPI

Django is one of three Python frameworks you'll meet constantly. They embody different philosophies, and knowing where each shines is a genuinely useful skill.

FrameworkPhilosophySweet spotTrade-off
Django Batteries-included, full-stack (MVT) Content sites, CRUD apps, anything needing an admin and auth More to learn up front; opinionated
Flask Micro-framework, minimal core Small services, when you want to pick every component You assemble the stack yourself
FastAPI Modern, async-first, type-driven High-performance JSON APIs, ML model serving Not a full web stack (no templates/admin by default)

Roughly speaking, Django sits between Flask's "assemble-it-yourself" minimalism and the strict conventions of Rails. It gives you a comprehensive toolkit but stays flexible about how you use it.

✅ A simple rule of thumb

Reach for Django when the app is data-heavy and you'll benefit from the admin and auth. Reach for Flask when the app is small and you want full control. Reach for FastAPI when the product is an API and raw async throughput matters.

The Request-Response Cycle

Every request that reaches a Django app travels through a fixed pipeline. Understanding the order of stages is the single most useful mental model for debugging — when something goes wrong, you can reason about where in the pipeline it happened.

graph TD A[Client Request] --> B[WSGI / ASGI Server] B --> C[Middleware chain - request phase] C --> D[URL Resolver] D --> E[View] E --> F[Model / Database] F --> G[Template Rendering] G --> H[Middleware chain - response phase] H --> I[HTTP Response to Client]
  1. WSGI/ASGI server (Gunicorn or Uvicorn) receives the raw request and hands it to Django.
  2. The request passes down through middleware — security checks, session loading, authentication.
  3. The URL resolver matches the path to a view.
  4. The view runs your logic, usually querying models.
  5. A template renders the response HTML (or a JSON serializer builds a response).
  6. The response travels back up through middleware — where headers, compression, and cookies are applied.
  7. The finished HTTP response returns to the client.

Middleware: hooks around every request

Middleware is a stack of components that wrap the view. Each one can inspect or modify the request on the way in and the response on the way out. Django ships with several essential ones — SecurityMiddleware, SessionMiddleware, AuthenticationMiddleware, and CsrfViewMiddleware among them.

Writing your own is straightforward — here is one that logs how long each request takes:

import time
import logging

logger = logging.getLogger(__name__)

class TimingMiddleware:
    def __init__(self, get_response):
        # Runs once when the server starts
        self.get_response = get_response

    def __call__(self, request):
        start = time.perf_counter()

        # Hand off to the next middleware or the view
        response = self.get_response(request)

        duration_ms = (time.perf_counter() - start) * 1000
        logger.info("%s %s -> %s (%.1f ms)",
                    request.method, request.path,
                    response.status_code, duration_ms)
        return response

You activate it by adding its dotted path to the MIDDLEWARE list in settings. Order matters: middleware runs top-to-bottom for requests and bottom-to-top for responses, so a component you want to run "first on the way in, last on the way out" belongs near the top.

WSGI vs. ASGI

Django historically spoke WSGI (synchronous). Modern Django also supports ASGI (asynchronous), which unlocks async views, WebSockets, and long-lived connections. A new project generated with Django 5 includes both wsgi.py and asgi.py, so you can choose your deployment path later.

WSGIASGI
Synchronous onlySync and async
HTTP onlyHTTP, WebSockets, and more
Served by Gunicorn / uWSGIServed by Uvicorn / Daphne

Projects, Apps & Structure

Django draws a sharp line between two concepts, and getting them straight early saves a lot of confusion:

  • A project is the whole web application — its settings, its URL root, and the collection of apps it runs.
  • An app is a self-contained, ideally reusable module that does one job — a blog, a shop, an accounts system.

A project contains many apps; a well-designed app could be dropped into a different project. Here is the layout a fresh Django 5 project produces:

myproject/                  # Project root (the "repository")
├── manage.py               # Command-line entry point
│
├── myproject/              # The project package (config lives here)
│   ├── __init__.py
│   ├── settings.py         # Configuration
│   ├── urls.py             # Root URL routing
│   ├── asgi.py             # Async server entry point
│   └── wsgi.py             # Sync server entry point
│
├── blog/                   # An app
│   ├── __init__.py
│   ├── admin.py            # Admin site registration
│   ├── apps.py             # App configuration
│   ├── migrations/         # Database schema history
│   ├── models.py           # Data models
│   ├── tests.py            # Tests
│   ├── urls.py             # App-level URL routing (you create this)
│   └── views.py            # Views
│
└── templates/              # Shared templates

The two commands that create these are:

# Create the project skeleton
django-admin startproject myproject .

# Create an app inside it
python manage.py startapp blog

Splitting a project into apps

Modularity is Django's superpower. A real e-commerce project might divide its concerns like this:

graph TD A[E-commerce Project] --> B[accounts] A --> C[catalog] A --> D[cart] A --> E[orders] A --> F[payments] B --> B1[User & profile models] C --> C1[Product & category models] D --> D1[Cart & line-item logic] E --> E1[Order & fulfilment] F --> F1[Checkout & providers]

✅ App design principles

  • Single responsibility — one app, one clear domain concept.
  • Loose coupling — minimize how much apps depend on each other.
  • Reusability — design as if the app might be extracted one day.

Settings & Configuration

A Django project's behavior is driven by settings.py. A few settings appear in nearly every project:

# myproject/settings.py
from pathlib import Path
import os

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

# Every app you use must be listed here
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",            # your own apps
]

MIDDLEWARE = [
    "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",
]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ["DB_PASSWORD"],
        "HOST": os.environ.get("DB_HOST", "127.0.0.1"),
        "PORT": os.environ.get("DB_PORT", "5432"),
    }
}

Never hard-code secrets

Notice that the database credentials above come from environment variables, not literal strings. The same rule applies to the SECRET_KEY and the DEBUG flag. Committing these to version control is one of the most common — and most dangerous — beginner mistakes.

# Read secrets from the environment, with safe defaults
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

# DEBUG must be False in production
DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"

ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")

# Harden HTTPS in production
if not DEBUG:
    SECURE_SSL_REDIRECT = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True
    SECURE_HSTS_SECONDS = 31_536_000  # one year

⚠️ The three settings that must change for production

  • DEBUG = False — leaving it True leaks stack traces and settings to attackers.
  • ALLOWED_HOSTS — set it to your real domains, never ['*'] in production.
  • SECRET_KEY — a unique, secret value pulled from the environment.

For anything beyond a toy project, split settings by environment. A common layout replaces settings.py with a package:

myproject/settings/
├── __init__.py
├── base.py          # Shared settings
├── development.py   # DEBUG=True, local database
└── production.py    # DEBUG=False, hardened security

You then point Django at the right one with an environment variable, so the same codebase behaves correctly everywhere:

# manage.py / wsgi.py
os.environ.setdefault(
    "DJANGO_SETTINGS_MODULE",
    "myproject.settings.production",
)

Hands-on: Scaffold a Project

🏋️ Build and trace your first Django project

Objective: Create a real Django 5 project, add an app, and follow a request from URL to response.

Instructions:

  1. Create and activate a virtual environment, then install Django:
    python -m venv venv
    source venv/bin/activate      # Windows: venv\Scripts\activate
    pip install "Django>=5,<6"
  2. Scaffold a project and an app:
    django-admin startproject myproject .
    python manage.py startapp pages
  3. Add "pages" to INSTALLED_APPS in settings.py.
  4. Write a view, wire up a URL, and run the server. Then visit http://127.0.0.1:8000/ and identify each MVT piece your request touched.
💡 Hint — where does the URL go?

The project's myproject/urls.py is the root URLconf. Use include() there to hand a path prefix off to your app's own urls.py. The app's URLconf then names the view that runs.

✅ Sample solution
# pages/views.py
from django.http import HttpResponse

def home(request):
    return HttpResponse("<h1>Hello from Django!</h1>")
# pages/urls.py  (create this file)
from django.urls import path
from . import views

urlpatterns = [
    path("", views.home, name="home"),
]
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("pages.urls")),
]
python manage.py migrate      # set up the built-in tables
python manage.py runserver

Your request hit the root URLconf → was include()d into pages.urls → matched the empty path → called home() (the view) → which returned an HttpResponse. No model or template this time — the smallest possible slice of MVT.

Best Practices

✅ Do

  • Keep each app focused on a single domain concept.
  • Read secrets from environment variables and keep them out of version control.
  • Commit your migrations — they are the history of your database schema.
  • Split settings by environment once the project outgrows a single file.
  • Let Django's built-in security middleware do its job; don't disable CSRF protection.

⚠️ Don't

  • Ship with DEBUG = True or ALLOWED_HOSTS = ['*'].
  • Put business logic in templates — templates present, views decide.
  • Build one giant "core" app that does everything.
  • Reach for Django for a tiny single-endpoint microservice — Flask or FastAPI may fit better.

💡 When to choose Django

Django rewards you when the app is content- or data-driven, has real user accounts, benefits from an admin panel, and needs to be secure and scalable. Consider a lighter framework for pure API microservices or highly specialized real-time systems.

Summary & Quiz

🎉 Key Takeaways

  • Django is a batteries-included Python framework — ORM, auth, admin, and security come built in.
  • It uses the MVT pattern; the "view" is the controller and the "template" renders HTML.
  • Every request flows down through middleware, to a URL-matched view, then back up as a response.
  • A project holds many focused, reusable apps.
  • Secrets belong in the environment, and production means DEBUG=False with a locked-down ALLOWED_HOSTS.

🎯 Quick Quiz

Question 1: In Django's MVT pattern, which component contains the request-handling logic (the "controller" of classic MVC)?

Question 2: Which setting is the most dangerous to leave enabled in production?

Question 3: What is the relationship between a Django project and a Django app?

📚 Further Reading

🚀 What's Next?

Now that you can see the whole machine, we'll zoom into its heart. In the next lesson, Data Modeling in Django, you'll define models, choose field types, wire up relationships, and let migrations build your database schema.

🎉 Great start!

You understand Django's architecture end to end. Let's give it some data to work with.