DJANGO
Backend

Django: Building Python Web Apps

Mayur Dabhi
Mayur Dabhi
June 2, 2026
14 min read

Django is Python's most battle-tested, full-featured web framework — powering Instagram, Pinterest, Disqus, and NASA's web properties at massive scale. If you've worked with Flask or FastAPI and found yourself repeatedly building the same scaffolding (authentication, admin panels, ORM, form validation), Django eliminates all of that boilerplate by including it out of the box. This guide takes you from zero to a working Django application, explaining the why behind every major concept along the way.

Why Django Over Flask or FastAPI?

Flask and FastAPI are excellent micro-frameworks that give you complete control — but you assemble the pieces yourself. Django's "batteries-included" philosophy ships with an ORM, admin interface, authentication system, form handling, migrations, and security middleware pre-integrated. For data-driven web applications, Django typically cuts development time in half.

The MVT Architecture

Django follows the Model-View-Template (MVT) pattern — a slight variation of MVC where Django's framework itself acts as the Controller. Understanding this separation is essential before writing your first line of Django code.

Browser HTTP Request URL Router urls.py View views.py Model models.py Template .html files DB Django MVT Request/Response Cycle

Django's MVT architecture: URL Router dispatches requests to Views, which interact with Models and render Templates

Installation and Project Setup

Django requires Python 3.10 or higher. Always use a virtual environment to isolate project dependencies — this prevents version conflicts across projects.

Requirement Minimum Recommended
Python 3.10 3.12+
Django 4.2 (LTS) 5.1+
Database SQLite (dev only) PostgreSQL 14+
pip 22.0 Latest
1

Create a Virtual Environment

Isolate your project dependencies using Python's built-in venv module or uv for faster installs.

Terminal
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate        # Linux/macOS
# venv\Scripts\activate         # Windows

# Install Django
pip install django

# Verify installation
python -m django --version      # Should print 5.x.x
2

Create a Django Project

The startproject command scaffolds the top-level configuration files for your Django site.

Terminal
# Create the project (trailing dot avoids a nested folder)
django-admin startproject mysite .

# Run the development server
python manage.py runserver
# → http://127.0.0.1:8000/
3

Create Your First App

Django projects are composed of reusable apps. Each app has its own models, views, and URLs — keeping the codebase modular.

Terminal
# Create a "blog" app inside your project
python manage.py startapp blog

Project Structure Explained

After running these commands, your project layout looks like this. Understanding each file's role saves hours of confusion later:

Project Layout
mysite/               ← project config package
    __init__.py
    settings.py       ← all project settings (DB, INSTALLED_APPS, etc.)
    urls.py           ← root URL configuration
    wsgi.py           ← WSGI entry point for production servers
    asgi.py           ← ASGI entry point for async servers

blog/                 ← your first app
    migrations/       ← auto-generated database migration files
    __init__.py
    admin.py          ← register models with Django admin
    apps.py           ← app configuration class
    models.py         ← database model definitions
    tests.py          ← unit and integration tests
    views.py          ← request handlers (business logic)
    urls.py           ← (you create this) app-level URL routing

manage.py             ← CLI tool for running dev server, migrations, etc.
4

Register the App in settings.py

Django won't discover your new app until you add it to INSTALLED_APPS in mysite/settings.py.

mysite/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',          # ← add your app here
]

Models and the ORM

Django's ORM (Object-Relational Mapper) lets you define your database schema using Python classes. Each class subclassing models.Model maps to a database table, and each class attribute maps to a column. You never have to write raw SQL for standard CRUD operations.

Defining Models

blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone


class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)

    class Meta:
        verbose_name_plural = "categories"

    def __str__(self):
        return self.name


class Post(models.Model):
    STATUS_CHOICES = [
        ('draft', 'Draft'),
        ('published', 'Published'),
    ]

    title      = models.CharField(max_length=250)
    slug       = models.SlugField(max_length=250, unique_for_date='publish')
    author     = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    category   = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
    body       = models.TextField()
    publish    = models.DateTimeField(default=timezone.now)
    created    = models.DateTimeField(auto_now_add=True)   # set once on creation
    updated    = models.DateTimeField(auto_now=True)       # updated on every save
    status     = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')

    class Meta:
        ordering = ['-publish']
        indexes  = [models.Index(fields=['-publish'])]

    def __str__(self):
        return self.title

Running Migrations

Migrations are Django's version control for your database schema. After any change to models.py, you run two commands:

Terminal
# Generate migration file from model changes
python manage.py makemigrations

# Apply pending migrations to the database
python manage.py migrate

# Inspect the SQL Django will run (without executing it)
python manage.py sqlmigrate blog 0001

Querying with the ORM

ORM Query Examples (Django Shell)
from blog.models import Post
from django.contrib.auth.models import User

# Retrieve all published posts (QuerySet is lazy — no DB hit yet)
published = Post.objects.filter(status='published')

# Chaining filters (AND logic)
recent = Post.objects.filter(
    status='published',
    publish__year=2026
).order_by('-publish')

# OR logic using Q objects
from django.db.models import Q
results = Post.objects.filter(
    Q(title__icontains='django') | Q(body__icontains='django')
)

# Select related (JOIN — avoids N+1 queries)
posts = Post.objects.select_related('author', 'category').all()

# Aggregate: count posts per category
from django.db.models import Count
Post.objects.values('category__name').annotate(total=Count('id'))

# Create a new post
user = User.objects.get(username='admin')
post = Post.objects.create(
    title='My First Post',
    slug='my-first-post',
    author=user,
    body='Hello, Django!',
    status='published',
)

# Update
Post.objects.filter(author=user).update(status='published')

# Delete
Post.objects.filter(status='draft', publish__lt=timezone.now() - timedelta(days=30)).delete()
Avoiding N+1 Query Problems

The most common Django performance mistake is loading related objects in a loop. Use select_related() for ForeignKey/OneToOne relationships (generates a SQL JOIN) and prefetch_related() for ManyToMany and reverse FK relationships (runs a separate optimized query). Always profile with Django Debug Toolbar before optimizing.

Views and URL Routing

Django views are Python callables (functions or class methods) that receive an HttpRequest and return an HttpResponse. URL patterns in urls.py map URL paths to view functions using regular expressions or simpler path converters.

Function-Based Views (FBVs)

blog/views.py
from django.shortcuts import render, get_object_or_404
from django.core.paginator import Paginator
from .models import Post


def post_list(request):
    posts = Post.objects.filter(status='published')
    paginator = Paginator(posts, 10)           # 10 posts per page
    page_number = request.GET.get('page', 1)
    page_obj = paginator.get_page(page_number)
    return render(request, 'blog/post_list.html', {'page_obj': page_obj})


def post_detail(request, year, month, day, slug):
    post = get_object_or_404(
        Post,
        status='published',
        slug=slug,
        publish__year=year,
        publish__month=month,
        publish__day=day,
    )
    return render(request, 'blog/post_detail.html', {'post': post})

Class-Based Views (CBVs)

Class-based views reduce boilerplate for common patterns like displaying lists, detail pages, and forms. Django's generic CBVs handle pagination, context, and queryset filtering with minimal code:

blog/views.py — Class-Based Equivalent
from django.views.generic import ListView, DetailView
from .models import Post


class PostListView(ListView):
    model = Post
    queryset = Post.objects.filter(status='published')
    context_object_name = 'posts'
    paginate_by = 10
    template_name = 'blog/post_list.html'


class PostDetailView(DetailView):
    model = Post
    context_object_name = 'post'
    template_name = 'blog/post_detail.html'

    def get_queryset(self):
        return Post.objects.filter(status='published')

URL Configuration

blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'   # namespace for URL reversing

urlpatterns = [
    path('', views.PostListView.as_view(), name='post_list'),
    path(
        '<int:year>/<int:month>/<int:day>/<slug:slug>/',
        views.PostDetailView.as_view(),
        name='post_detail',
    ),
]
mysite/urls.py — Root Router
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls', namespace='blog')),
]

Django Templates

Django's template language is intentionally simple — it's designed for HTML authors, not programmers. The key feature is template inheritance: define a base layout once and extend it in child templates, keeping navigation, headers, and footers DRY.

templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}My Blog{% endblock %}</title>
    <link rel="stylesheet" href="{% static 'css/styles.css' %}">
{% load static %}
</head>
<body>
    <nav>...</nav>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>...</footer>
</body>
</html>
templates/blog/post_list.html
{% extends 'base.html' %}

{% block title %}Latest Posts | My Blog{% endblock %}

{% block content %}
<h1>Latest Posts</h1>

{% for post in page_obj %}
  <article>
    <h2>
      <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
    </h2>
    <p>By {{ post.author.get_full_name }} on {{ post.publish|date:"F j, Y" }}</p>
    <p>{{ post.body|truncatewords:30 }}</p>
  </article>
{% empty %}
  <p>No posts published yet.</p>
{% endfor %}

{# Pagination #}
{% if page_obj.has_other_pages %}
  <nav>
    {% if page_obj.has_previous %}
      <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
    {% endif %}
    <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
    {% if page_obj.has_next %}
      <a href="?page={{ page_obj.next_page_number }}">Next</a>
    {% endif %}
  </nav>
{% endif %}
{% endblock %}

Django Admin Interface

One of Django's most powerful features is the automatic admin interface. Register your models and get a fully functional CRUD interface immediately — no frontend code required. This is genuinely useful in production for content management and data inspection.

blog/admin.py
from django.contrib import admin
from .models import Category, Post


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ['name', 'slug']
    prepopulated_fields = {'slug': ('name',)}


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display  = ['title', 'author', 'status', 'publish']
    list_filter   = ['status', 'created', 'publish', 'author']
    search_fields = ['title', 'body']
    prepopulated_fields = {'slug': ('title',)}
    raw_id_fields = ['author']       # search widget instead of dropdown
    date_hierarchy = 'publish'
    ordering      = ['status', '-publish']
    list_editable = ['status']       # edit status inline in the list view
Terminal — Create Admin User
python manage.py createsuperuser
# → Visit http://127.0.0.1:8000/admin/ to log in

Authentication and Security

Django ships with a complete authentication system: user accounts, password hashing (PBKDF2 by default), login/logout views, password reset via email, and permission-based access control. You rarely need a third-party library for standard auth.

mysite/urls.py — Include Auth URLs
from django.contrib.auth import views as auth_views

urlpatterns += [
    # Built-in login/logout/password reset views
    path('accounts/', include('django.contrib.auth.urls')),
]
# Provides: /accounts/login/  /accounts/logout/
#           /accounts/password_change/  /accounts/password_reset/
blog/views.py — Protecting Views
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin


# Function-based view protection
@login_required
def create_post(request):
    ...


# Class-based view protection
class CreatePostView(LoginRequiredMixin, CreateView):
    model = Post
    fields = ['title', 'body', 'status']
    # LoginRequiredMixin redirects unauthenticated users to settings.LOGIN_URL
Security Checklist for Production

Before deploying, set DEBUG = False in settings, configure ALLOWED_HOSTS, use environment variables for SECRET_KEY and database credentials (never commit them to git), serve static files via a CDN or Nginx, and enable HTTPS. Run python manage.py check --deploy to get Django's built-in security audit.

Django's Built-in Security Features

Deploying a Django Application

Django ships with a development server (runserver) that is not suitable for production. The standard production stack uses Gunicorn as the WSGI application server behind Nginx as a reverse proxy.

Component Role Alternative
Gunicorn WSGI/ASGI server — runs Python code uWSGI, Daphne (async)
Nginx Reverse proxy, static files, SSL termination Caddy, Apache
PostgreSQL Production database MySQL, MariaDB
Redis Cache backend, session storage, Celery broker Memcached
Celery Asynchronous task queue Django Q, Dramatiq
Terminal — Install and Run Gunicorn
pip install gunicorn psycopg2-binary whitenoise

# Collect static files into STATIC_ROOT
python manage.py collectstatic

# Run with 4 worker processes
gunicorn mysite.wsgi:application --workers 4 --bind 0.0.0.0:8000
mysite/settings.py — Production Settings
import os

SECRET_KEY = os.environ['DJANGO_SECRET_KEY']   # never hardcode!
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# PostgreSQL
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', 'localhost'),
        'PORT': '5432',
    }
}

# WhiteNoise serves static files efficiently without Nginx
MIDDLEWARE = ['whitenoise.middleware.WhiteNoiseMiddleware', ...rest...]
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# Redis cache
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': os.environ.get('REDIS_URL', 'redis://127.0.0.1:6379/1'),
    }
}

# Security headers
SECURE_SSL_REDIRECT         = True
SECURE_HSTS_SECONDS         = 31536000
SESSION_COOKIE_SECURE       = True
CSRF_COOKIE_SECURE          = True

Conclusion

Django's "batteries-included" philosophy means you spend far less time assembling infrastructure and far more time building the features that make your application unique. The ORM eliminates 90% of raw SQL, the admin interface handles internal tooling out of the box, the auth system is production-hardened, and the migration engine keeps your schema in sync with your code across every environment.

The path forward from here is rich: explore Django REST Framework (DRF) for building APIs consumed by React or mobile apps, Django Channels for WebSocket-powered real-time features, Celery for background job processing, and django-allauth for social authentication. Each library integrates naturally with Django's architecture because it was designed for exactly this kind of ecosystem growth.

Key Takeaways

  • Django's MVT pattern cleanly separates data (Model), logic (View), and presentation (Template)
  • The ORM handles migrations, CRUD, and complex queries — use select_related() to avoid N+1 problems
  • URL namespacing with app_name and {% url %} tags keeps links refactor-safe
  • Template inheritance via {% extends %} keeps your HTML DRY across all pages
  • The built-in admin, auth, and CSRF/XSS protection give you a production-ready security baseline for free
  • Deploy with Gunicorn + Nginx, PostgreSQL, and always externalize secrets via environment variables
Python Django Web Development MVT ORM Backend Tutorial
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.