π§© Django Applications Structure
A Django project is a container; the real work happens in apps β small, focused, reusable packages of functionality. This lesson shows you how to create an app, what every file inside it is for, how to extend it, and how to design apps that stay clean as your project grows.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a Django app is and how it differs from a project
- Create an app with
startappand describe the purpose of each generated file - Extend the default structure with
urls.py,forms.py, templates, and static files - Apply design principles β single responsibility, loose coupling, reusability
- Choose an appropriate communication pattern (foreign keys, signals, template tags) between apps
Estimated Time: 40β50 minutes β’ Difficulty: Intermediate
Hands-on: Scaffold a blog app, register it, and extend its structure.
In This Lesson
The App-Centric Approach
A Django app is a Python package that provides one slice of a website's functionality β a blog, a shopping cart, an accounts system. A project is the configuration and collection of apps that make up a whole site. One project holds many apps; a well-designed app can be dropped into other projects.
The four hallmarks of a good app:
- Self-contained β it bundles the models, views, and templates its feature needs.
- Reusable β it can be installed into another project with minimal changes.
- Focused β it does one thing well (the single-responsibility idea).
- Maintainable β its structure makes it easy to find and change things.
π¬ Analogy: The project is a shopping mall; each app is a store. Every store has its own inventory and staff (models and views) and can operate on its own, but all of them share the mall's infrastructure (Django's core, the database, the URL system).
Creating & Registering an App
Use the startapp management command from the folder containing manage.py:
python manage.py startapp blog
Creating the folder is only half the job β Django ignores an app until you register it in settings.py. The recommended form points at the app's config class:
# myproject/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Your apps
"blog.apps.BlogConfig", # or simply "blog"
]
β οΈ The most common beginner bug
If migrations, the admin, or templates for your new app seem to do nothing, you almost certainly forgot to add it to INSTALLED_APPS. Register the app first, then run makemigrations.
Anatomy of an App
startapp blog generates this structure:
blog/
βββ __init__.py
βββ admin.py
βββ apps.py
βββ migrations/
β βββ __init__.py
βββ models.py
βββ tests.py
βββ views.py
| File | Purpose |
|---|---|
__init__.py | Marks the folder as a package. Usually empty. |
admin.py | Register models so they appear in the admin site. |
apps.py | App configuration (the AppConfig class). |
migrations/ | Version-controlled database schema changes. |
models.py | Your data models β one class per table. |
tests.py | Automated tests for the app. |
views.py | Request-handling logic. |
models.py β the data
Models are the heart of an app. Each class becomes a table; each attribute a column. Here's a small blog schema using modern Django 5 conventions:
from django.db import models
from django.conf import settings
from django.urls import reverse
from django.utils import timezone
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(max_length=100, unique=True)
class Meta:
verbose_name_plural = "categories"
def __str__(self):
return self.name
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
title = models.CharField(max_length=250)
slug = models.SlugField(max_length=250, unique_for_date="published_date")
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="blog_posts",
)
content = models.TextField()
published_date = models.DateTimeField(default=timezone.now)
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
status = models.CharField(
max_length=10, choices=Status.choices, default=Status.DRAFT
)
categories = models.ManyToManyField(Category, related_name="posts")
class Meta:
ordering = ["-published_date"]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("blog:post_detail", kwargs={"slug": self.slug})
π Modern touches to notice
models.TextChoices is the current, readable way to define choices (replacing raw tuples).
settings.AUTH_USER_MODEL is preferred over importing User directly β it keeps the app working even with a custom user model.
views.py β the logic
Django offers both function-based and class-based views. Class-based generic views remove boilerplate for common patterns like "list" and "detail":
from django.shortcuts import get_object_or_404, render
from django.views.generic import ListView, DetailView
from .models import Post, Category
class PostListView(ListView):
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(status=Post.Status.PUBLISHED)
class PostDetailView(DetailView):
template_name = "blog/post_detail.html"
context_object_name = "post"
def get_queryset(self):
return Post.objects.filter(status=Post.Status.PUBLISHED)
def category_posts(request, slug):
"""Function-based view: posts in one category."""
category = get_object_or_404(Category, slug=slug)
posts = category.posts.filter(status=Post.Status.PUBLISHED)
return render(
request,
"blog/category_posts.html",
{"category": category, "posts": posts},
)
admin.py β free CRUD
from django.contrib import admin
from .models import Post, Category
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ("title", "author", "published_date", "status")
list_filter = ("status", "created_date", "author")
search_fields = ("title", "content")
prepopulated_fields = {"slug": ("title",)}
date_hierarchy = "published_date"
ordering = ("status", "-published_date")
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ("name", "slug")
prepopulated_fields = {"slug": ("name",)}
Extending the Structure
The default files are a starting point. Real apps add more as they grow:
urls.py β per-app routing
Not created by default, but almost always added. Giving each app its own URL file with an app_name namespace keeps routing modular:
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.PostListView.as_view(), name="post_list"),
path("post/<slug:slug>/", views.PostDetailView.as_view(), name="post_detail"),
path("category/<slug:slug>/", views.category_posts, name="category_posts"),
]
Then include it from the project's main urls.py:
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
]
forms.py β validation & rendering
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "content", "status", "categories"]
widgets = {
"title": forms.TextInput(attrs={"class": "form-control"}),
"content": forms.Textarea(attrs={"class": "form-control"}),
"categories": forms.CheckboxSelectMultiple(),
}
templates/ and static/
Namespace both under a subfolder named for the app to avoid collisions between apps:
blog/
βββ templates/
β βββ blog/
β βββ post_list.html
β βββ post_detail.html
βββ static/
βββ blog/
βββ css/style.css
βββ js/blog.js
π‘ Why the doubled folder name?
templates/blog/post_list.html looks redundant, but Django merges every app's templates/ into one search path. The blog/ prefix is what makes "blog/post_list.html" unambiguous when two apps both have a post_list.html.
App Configuration
The apps.py file defines an AppConfig subclass that controls app-level behavior. Its ready() method is the standard place to connect signal handlers:
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog" # required: the dotted Python path
verbose_name = "Blog System" # optional: human-readable name
def ready(self):
# Import signal handlers so they get connected on startup.
# Import inside ready() to avoid app-registry import loops.
from . import signals # noqa: F401
β οΈ Don't import models at module top-level in apps.py
Importing models when apps.py is first read runs before the app registry is ready and causes import errors. Always import models inside ready() or other methods.
π¬ Analogy:AppConfigis the store's operating manual β its official name, its signage, and the "opening procedure" (ready()) that runs when the store opens for business.
App Design Principles
Good apps stay small and independent. Three principles keep a growing codebase sane.
1. Single responsibility
Each app should do one thing. Rather than a giant accounts app that handles profiles, permissions, and notifications, split it:
usersβ account managementprofilesβ profile data and settingsnotificationsβ the notification system
2. Loose coupling
Apps should depend on each other as little as possible, and interact through clear interfaces. Prefer reverse() and signals over hard-wired assumptions.
3. Reusability
Avoid project-specific hard-coding. The classic example is URLs β never build them by hand:
π« Don't hard-code URLs
def get_absolute_url(self):
return f"/blog/post/{self.slug}/" # breaks if the URL ever changes
β Use reverse() / named URLs
from django.urls import reverse
def get_absolute_url(self):
return reverse("blog:post_detail", kwargs={"slug": self.slug})
π‘ Consistent naming pays off
For a Post model, name related pieces predictably: PostForm, PostListView, PostDetailView, and templates post_list.html / post_detail.html. Newcomers can then guess where things live.
Communication Between Apps
Apps rarely live in total isolation. Pick the lightest pattern that does the job.
| Pattern | Use it when⦠| Coupling |
|---|---|---|
| Foreign keys / relationships | One app's data genuinely relates to another's. | Tight (by design) |
| Signals | You want to react to an event without a hard dependency. | Loose |
| Template tags | An app should offer reusable snippets to any template. | Loose |
| Middleware | You need to act on every request/response. | Global |
Foreign keys β the direct link
# A Comment in one app points at a Post in another.
from django.db import models
from blog.models import Post
class Comment(models.Model):
post = models.ForeignKey(
Post, on_delete=models.CASCADE, related_name="comments"
)
body = models.TextField()
created = models.DateTimeField(auto_now_add=True)
Signals β react without coupling
Signals let one app respond to another's events without importing its logic. For example, notify a post's author when a comment arrives:
# notifications/receivers.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from blog.models import Comment
from .models import Notification
@receiver(post_save, sender=Comment)
def notify_on_comment(sender, instance, created, **kwargs):
if created:
Notification.objects.create(
user=instance.post.author,
message=f"New comment on: {instance.post.title}",
url=instance.post.get_absolute_url(),
)
Template tags β reusable snippets
# blog/templatetags/blog_tags.py
from django import template
from ..models import Post
register = template.Library()
@register.inclusion_tag("blog/tags/recent_posts.html")
def show_recent_posts(count=5):
posts = Post.objects.filter(status=Post.Status.PUBLISHED)[:count]
return {"recent_posts": posts}
Used from any template in any app:
{% load blog_tags %}
<aside>
<h3>Recent Posts</h3>
{% show_recent_posts 3 %}
</aside>
π¬ Analogy: Foreign keys are a formal contract between two stores; signals are the mall PA system announcing an event any store can react to; template tags are a shared kiosk any store can display in its window.
Hands-on Exercise
ποΈ Scaffold and Wire a Blog App
Objective: Turn the empty project from the last lesson into one with a working, registered blog app whose URLs are reachable.
Instructions:
- Run
startapp blog. - Register it in
INSTALLED_APPSusing its config class. - Add a
CategoryandPostmodel (use the code above as a guide). - Register both models in
admin.py. - Create
blog/urls.pywith anapp_nameand onepost_listroute, theninclude()it from the project. - Run
makemigrations blogandmigrate, then add a post through the admin.
π‘ Hint
If makemigrations reports "No changes detected", Django can't see your app β double-check the INSTALLED_APPS entry and that your models are in blog/models.py. Run makemigrations blog (naming the app) to be sure it's scanned.
β Solution (key steps)
python manage.py startapp blog
# settings.py -> INSTALLED_APPS += ["blog.apps.BlogConfig"]
# add models to blog/models.py, register them in blog/admin.py
# create blog/urls.py with app_name = "blog"
# project urls.py -> path("blog/", include("blog.urls"))
python manage.py makemigrations blog
python manage.py migrate
python manage.py runserver
# visit /admin/ to add a Category + a published Post,
# then /blog/ to see it listed
π― Quick Quiz
Question 1: After running startapp blog, why might your app still seem to do nothing?
Question 2: Which pattern lets one app react to another app's events with the loosest coupling?
Question 3: What's the recommended way for a model to return its own page URL?
Summary & Quiz
π Key Takeaways
- Apps are focused, reusable packages; a project is the collection of apps plus configuration.
startappscaffolds the app, but you must register it inINSTALLED_APPSfor Django to see it.- Extend the defaults with
urls.py,forms.py, namespaced templates and static files, and more. - Design for single responsibility, loose coupling, and reusability β and use
reverse(), not hard-coded URLs. - Choose the lightest communication pattern: foreign keys for real relationships, signals for loose reactions, template tags for shared snippets.
π Further Reading
- Django docs β applications & AppConfig
- Writing your first reusable app
- Django docs β signals
- Custom template tags & filters
π What's Next?
You can create and organize apps β now it's time to give them real data. In the next lesson, Django Model Definition, you'll go deep on models, field types, relationships, and the migration workflow.
π Your project has structure!
Apps are the building blocks of every Django site. Next: filling them with data.