March 26, 202613 min read

Django Tutorial: Build a Real Web App (Not a To-Do List)

Build a real web application with Django. Covers project setup, models, views, templates, authentication, forms, and deployment from scratch.

django python web-development backend tutorial
Ad 336x280

Every Django tutorial builds a to-do list or a poll app. This one doesn't. We're going to build a link bookmarking app -- something you'd actually use -- where users can save links, organize them into collections, and share collections publicly. Along the way, you'll learn every core Django concept.

Django is a Python web framework that follows the "batteries included" philosophy. It gives you an ORM, an admin panel, authentication, form handling, URL routing, and templating out of the box. That might sound like a lot, but it means you spend time building features instead of wiring up infrastructure.

Why Django

Before we write code, here's why Django is worth learning:

  • Speed of development: Features that take days in other frameworks take hours in Django. The admin panel alone saves weeks of work.
  • Security defaults: CSRF protection, SQL injection prevention, XSS protection, and clickjacking protection are built in and enabled by default.
  • ORM: Write Python, get SQL. Migrations are handled automatically.
  • Scale: Instagram serves 2 billion+ users on Django. It scales.
  • Ecosystem: Thousands of packages for payments (Stripe), search (Elasticsearch), REST APIs (DRF), and more.
The tradeoff: Django has opinions. It wants you to organize code a certain way. If you fight the framework, you'll have a bad time. If you work with it, you'll be incredibly productive.

Project Setup

Install Django

# Create a virtual environment
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# Install Django
pip install django

# Verify
python -m django --version

Create the Project

django-admin startproject bookmarks_project .

The . at the end creates the project in the current directory (no extra nested folder).

Your structure:

bookmarks_project/
    __init__.py
    settings.py      # Configuration
    urls.py           # Root URL routing
    asgi.py           # ASGI entry point
    wsgi.py           # WSGI entry point
manage.py             # CLI tool for everything

Create the App

Django projects contain apps. An app is a module that does one thing.

python manage.py startapp links

Register it in settings.py:

# bookmarks_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'links',  # Add your app here
]

Run the Development Server

python manage.py runserver

Visit http://127.0.0.1:8000/ -- you should see the Django welcome page. You have a working web application.

Models: Defining Your Data

Models are Python classes that map to database tables. Django's ORM handles the SQL.

# links/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils.text import slugify

class Collection(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE,
related_name='collections')
is_public = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
ordering = ['-updated_at']

def __str__(self):
return self.name

def save(self, args, *kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(args, *kwargs)

class Link(models.Model):
url = models.URLField(max_length=2000)
title = models.CharField(max_length=300)
description = models.TextField(blank=True)
collection = models.ForeignKey(Collection, on_delete=models.CASCADE,
related_name='links')
added_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
ordering = ['-created_at']

def __str__(self):
return self.title

Migrations: Syncing Models to the Database

python manage.py makemigrations  # Generate migration files
python manage.py migrate         # Apply them to the database

Every time you change a model, run both commands. Django tracks changes and generates the SQL automatically.

Key Model Field Types

FieldPurpose
CharField(max_length=N)Short text with a maximum length
TextField()Unlimited text
IntegerField()Whole numbers
BooleanField()True/False
DateTimeField()Date + time
URLField()URL with validation
SlugField()URL-friendly string
ForeignKey()Relationship to another model
ManyToManyField()Many-to-many relationship

Querying with the ORM

# Create
collection = Collection.objects.create(
    name="Python Resources",
    owner=user,
    is_public=True
)

# Read
all_collections = Collection.objects.all()
public_collections = Collection.objects.filter(is_public=True)
my_collection = Collection.objects.get(slug="python-resources")

# Update
my_collection.description = "Useful Python links"
my_collection.save()

# Delete
my_collection.delete()

# Chaining
recent_public = (Collection.objects
    .filter(is_public=True)
    .order_by('-created_at')[:10])

# Related objects
links_in_collection = my_collection.links.all()
user_collections = user.collections.count()

The Admin Panel

Django's admin is legendary. Register your models and you get a full CRUD interface for free.

# links/admin.py
from django.contrib import admin
from .models import Collection, Link

@admin.register(Collection)
class CollectionAdmin(admin.ModelAdmin):
list_display = ['name', 'owner', 'is_public', 'created_at']
list_filter = ['is_public', 'created_at']
search_fields = ['name', 'description']
prepopulated_fields = {'slug': ('name',)}

@admin.register(Link)
class LinkAdmin(admin.ModelAdmin):
list_display = ['title', 'url', 'collection', 'added_by', 'created_at']
list_filter = ['collection', 'created_at']
search_fields = ['title', 'url', 'description']

Create a superuser to access the admin:

python manage.py createsuperuser

Visit http://127.0.0.1:8000/admin/ and log in. You can now create, edit, and delete collections and links through a polished UI. For many internal tools, the admin panel is the entire product.

URL Routing

Django routes URLs to views. You define patterns in urls.py files.

# bookmarks_project/urls.py
from django.contrib import admin
from django.urls import path, include

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

# links/urls.py
from django.urls import path
from . import views

app_name = 'links'

urlpatterns = [
path('', views.home, name='home'),
path('collections/', views.collection_list, name='collection_list'),
path('collections/new/', views.collection_create, name='collection_create'),
path('collections/<slug:slug>/', views.collection_detail,
name='collection_detail'),
path('collections/<slug:slug>/add/', views.link_add, name='link_add'),
path('explore/', views.explore, name='explore'),
]

app_name enables namespaced URLs, so you can reference them as links:home in templates and code.

Views: Handling Requests

Views are functions (or classes) that receive a request and return a response. Let's build them all.

# links/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Collection, Link
from .forms import CollectionForm, LinkForm

def home(request):
"""Landing page."""
if request.user.is_authenticated:
collections = Collection.objects.filter(owner=request.user)
return render(request, 'links/home.html', {
'collections': collections
})
return render(request, 'links/landing.html')

def explore(request):
"""Browse public collections."""
collections = Collection.objects.filter(is_public=True)
return render(request, 'links/explore.html', {
'collections': collections
})

def collection_detail(request, slug):
"""View a collection and its links."""
collection = get_object_or_404(Collection, slug=slug)

# Only the owner can see private collections if not collection.is_public and collection.owner != request.user: return redirect('links:home')

links = collection.links.all()
return render(request, 'links/collection_detail.html', {
'collection': collection,
'links': links,
})

@login_required
def collection_list(request):
"""List the current user's collections."""
collections = Collection.objects.filter(owner=request.user)
return render(request, 'links/collection_list.html', {
'collections': collections
})

@login_required
def collection_create(request):
"""Create a new collection."""
if request.method == 'POST':
form = CollectionForm(request.POST)
if form.is_valid():
collection = form.save(commit=False)
collection.owner = request.user
collection.save()
messages.success(request, f'Collection "{collection.name}" created.')
return redirect('links:collection_detail', slug=collection.slug)
else:
form = CollectionForm()

return render(request, 'links/collection_form.html', {
'form': form,
'title': 'New Collection'
})

@login_required
def link_add(request, slug):
"""Add a link to a collection."""
collection = get_object_or_404(Collection, slug=slug, owner=request.user)

if request.method == 'POST':
form = LinkForm(request.POST)
if form.is_valid():
link = form.save(commit=False)
link.collection = collection
link.added_by = request.user
link.save()
messages.success(request, f'Link added to {collection.name}.')
return redirect('links:collection_detail', slug=slug)
else:
form = LinkForm()

return render(request, 'links/link_form.html', {
'form': form,
'collection': collection,
})

Key patterns here:


  • get_object_or_404: fetch an object or return 404. Never manually check if not exists.

  • @login_required: redirects anonymous users to the login page.

  • commit=False: save the form to get a model instance without hitting the database, so you can add extra fields.

  • The messages framework: flash messages that display once and disappear.


Forms

Django forms handle validation, rendering, and security (CSRF) automatically.

# links/forms.py
from django import forms
from .models import Collection, Link

class CollectionForm(forms.ModelForm):
class Meta:
model = Collection
fields = ['name', 'description', 'is_public']
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-input',
'placeholder': 'Collection name'
}),
'description': forms.Textarea(attrs={
'class': 'form-textarea',
'rows': 3,
'placeholder': 'Optional description'
}),
}

class LinkForm(forms.ModelForm):
class Meta:
model = Link
fields = ['url', 'title', 'description']
widgets = {
'url': forms.URLInput(attrs={
'class': 'form-input',
'placeholder': 'https://...'
}),
'title': forms.TextInput(attrs={
'class': 'form-input',
'placeholder': 'Link title'
}),
'description': forms.Textarea(attrs={
'class': 'form-textarea',
'rows': 2,
'placeholder': 'Why is this link useful?'
}),
}

ModelForm generates form fields from your model. It handles validation (URL format, required fields, max length) automatically. You customize with widgets for HTML attributes and fields to control which model fields appear.

Templates

Create the template directory structure:

links/templates/links/
    base.html
    landing.html
    home.html
    explore.html
    collection_list.html
    collection_detail.html
    collection_form.html
    link_form.html

Base Template

<!-- links/templates/links/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Bookmarks{% endblock %}</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; line-height: 1.6;
               max-width: 800px; margin: 0 auto; padding: 20px; }
        nav { display: flex; gap: 16px; padding: 16px 0;
              border-bottom: 1px solid #e5e5e5; margin-bottom: 24px; }
        nav a { text-decoration: none; color: #2563eb; }
        .btn { display: inline-block; padding: 8px 16px; background: #2563eb;
               color: white; text-decoration: none; border-radius: 6px;
               border: none; cursor: pointer; }
        .form-input, .form-textarea { width: 100%; padding: 8px 12px;
               border: 1px solid #d1d5db; border-radius: 6px;
               margin-bottom: 12px; }
        .message { padding: 12px; border-radius: 6px; margin-bottom: 16px;
                   background: #dcfce7; color: #166534; }
    </style>
</head>
<body>
    <nav>
        <a href="{% url 'links:home' %}">Home</a>
        <a href="{% url 'links:explore' %}">Explore</a>
        {% if user.is_authenticated %}
            <a href="{% url 'links:collection_list' %}">My Collections</a>
            <a href="{% url 'links:collection_create' %}">New Collection</a>
            <a href="{% url 'logout' %}">Logout</a>
        {% else %}
            <a href="{% url 'login' %}">Login</a>
        {% endif %}
    </nav>

{% for message in messages %}
<div class="message">{{ message }}</div>
{% endfor %}

{% block content %}{% endblock %}
</body>
</html>

Collection Detail Template

<!-- links/templates/links/collection_detail.html -->
{% extends "links/base.html" %}

{% block title %}{{ collection.name }}{% endblock %}

{% block content %}
<h1>{{ collection.name }}</h1>
{% if collection.description %}
<p>{{ collection.description }}</p>
{% endif %}
<p>
By {{ collection.owner.username }}
{% if collection.is_public %}(Public){% else %}(Private){% endif %}
</p>

{% if user == collection.owner %}
<a href="{% url 'links:link_add' collection.slug %}" class="btn">
Add Link
</a>
{% endif %}

<div style="margin-top: 24px;">
{% for link in links %}
<div style="padding: 16px 0; border-bottom: 1px solid #e5e5e5;">
<a href="{{ link.url }}" target="_blank" rel="noopener">
<strong>{{ link.title }}</strong>
</a>
<br>
<small style="color: #6b7280;">{{ link.url|truncatechars:60 }}</small>
{% if link.description %}
<p>{{ link.description }}</p>
{% endif %}
</div>
{% empty %}
<p>No links yet. Add some!</p>
{% endfor %}
</div>
{% endblock %}

Form Template (Reusable)

<!-- links/templates/links/collection_form.html -->
{% extends "links/base.html" %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
<h1>{{ title }}</h1>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div style="margin-bottom: 16px;">
<label for="{{ field.id_for_label }}">
<strong>{{ field.label }}</strong>
</label>
{{ field }}
{% if field.errors %}
<p style="color: red;">{{ field.errors.0 }}</p>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn">Save</button>
</form>
{% endblock %}

{% csrf_token %} is required on every POST form. It prevents cross-site request forgery attacks. Django will reject the form without it.

Authentication

Django has authentication built in. You just need to wire up the URLs and templates.

# bookmarks_project/urls.py
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views

urlpatterns = [
path('admin/', admin.site.urls),
path('login/', auth_views.LoginView.as_view(
template_name='links/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(
next_page='/'), name='logout'),
path('', include('links.urls')),
]

Add to settings.py:

LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/'

Create the login template:

<!-- links/templates/links/login.html -->
{% extends "links/base.html" %}

{% block title %}Login{% endblock %}

{% block content %}
<h1>Login</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn">Log In</button>
</form>
{% endblock %}

That's it. Full authentication with session management, password hashing, and CSRF protection. Django handles all of it.

For user registration, you'd create a view with Django's UserCreationForm:

from django.contrib.auth.forms import UserCreationForm

def signup(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect('links:home')
else:
form = UserCreationForm()
return render(request, 'links/signup.html', {'form': form})

Deploying Your Django App

Django apps are typically deployed with:

  1. Gunicorn as the WSGI server (replaces Django's dev server)
  2. PostgreSQL as the database (replaces SQLite)
  3. Nginx or a cloud load balancer in front
  4. Static files served separately (WhiteNoise for simplicity, S3/CDN for scale)

Preparing for Production

# settings.py changes for production
import os

DEBUG = False # NEVER True in production
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')

# Database DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': os.environ.get('DB_NAME'), 'USER': os.environ.get('DB_USER'), 'PASSWORD': os.environ.get('DB_PASSWORD'), 'HOST': os.environ.get('DB_HOST', 'localhost'), 'PORT': '5432', } } # Static files STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

Quick Deploy with Railway/Render

Modern platforms make deployment simple:

# requirements.txt
django==5.1
gunicorn==23.0.0
psycopg2-binary==2.9.9
whitenoise==6.7.0

# Procfile (for Railway/Render)
web: gunicorn bookmarks_project.wsgi

Add WhiteNoise to middleware for static file serving:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # Add this
    # ... rest of middleware
]

Then push to Git and connect to Railway, Render, or Fly.io. They auto-detect Django and deploy it.

Django vs Other Frameworks

DjangoFlaskFastAPIExpress
LanguagePythonPythonPythonJavaScript
PhilosophyBatteries includedMicro (bring your own)Modern, asyncMinimal
ORMBuilt-inSQLAlchemy (add-on)SQLAlchemy (add-on)Prisma/Sequelize
AdminBuilt-inFlask-Admin (add-on)NoneNone
AuthBuilt-inFlask-Login (add-on)Roll your ownPassport.js
Best forFull web appsAPIs, microservicesAPIs, asyncAPIs, real-time
Learning curveMediumLowLowLow
Choose Django when you're building a complete web application with user accounts, admin needs, and database-backed features. Choose Flask or FastAPI when you're building APIs or microservices where you want more control. Choose Express when your team is JavaScript-focused.

Common Django Patterns

Class-Based Views (Alternative Approach)

from django.views.generic import ListView, DetailView, CreateView

class CollectionListView(ListView):
model = Collection
template_name = 'links/collection_list.html'
context_object_name = 'collections'

def get_queryset(self):
return Collection.objects.filter(owner=self.request.user)

Class-based views reduce boilerplate for standard CRUD operations. Function-based views are better when you need custom logic. Use both -- they're not mutually exclusive.

Management Commands

# links/management/commands/cleanup_empty.py
from django.core.management.base import BaseCommand
from links.models import Collection

class Command(BaseCommand):
help = 'Delete collections with no links'

def handle(self, args, *options):
empty = Collection.objects.filter(links__isnull=True)
count = empty.count()
empty.delete()
self.stdout.write(f'Deleted {count} empty collections')

python manage.py cleanup_empty

Signals (React to Events)

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_default_collection(sender, instance, created, **kwargs):
if created:
Collection.objects.create(
name="My Bookmarks",
owner=instance,
)

Every new user automatically gets a default collection.

Next Steps

  1. Django REST Framework: Build APIs that mobile apps and SPAs can consume
  2. Testing: Django's test framework is excellent -- TestCase, Client, and factory patterns
  3. Caching: Django's cache framework supports Redis, Memcached, and database caching
  4. Celery: Background task processing for emails, data processing, etc.
  5. Channels: WebSocket support for real-time features
Django has been around since 2005 and it's still one of the most productive frameworks you can use. The ecosystem is mature, the documentation is exceptional, and the community is large. Start building.

For more web development tutorials, framework guides, and programming content, check out CodeUp.

Ad 728x90