🎸 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 term | Responsibility | Classic MVC equivalent |
|---|---|---|
| Model | Data structure and database access | Model |
| View | Request handling and business logic | Controller |
| Template | Presentation — rendering HTML | View |
| URLconf | Maps 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/:
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
- A user requests
example.com/blog/post/123/. - The URLconf matches the pattern and calls the matching view.
- The view uses a model to fetch post #123 from the database.
- The view hands the post data to a template as "context".
- 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.
| Framework | Philosophy | Sweet spot | Trade-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.
- WSGI/ASGI server (Gunicorn or Uvicorn) receives the raw request and hands it to Django.
- The request passes down through middleware — security checks, session loading, authentication.
- The URL resolver matches the path to a view.
- The view runs your logic, usually querying models.
- A template renders the response HTML (or a JSON serializer builds a response).
- The response travels back up through middleware — where headers, compression, and cookies are applied.
- 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.
| WSGI | ASGI |
|---|---|
| Synchronous only | Sync and async |
| HTTP only | HTTP, WebSockets, and more |
| Served by Gunicorn / uWSGI | Served 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:
✅ 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 itTrueleaks 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:
- 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" - Scaffold a project and an app:
django-admin startproject myproject . python manage.py startapp pages - Add
"pages"toINSTALLED_APPSinsettings.py. - 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 = TrueorALLOWED_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=Falsewith a locked-downALLOWED_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.