Skip to main content

🧩 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 startapp and 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.

graph LR A[Django Project] --> B[blog app] A --> C[accounts app] A --> D[shop app] A --> E[api app]

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
FilePurpose
__init__.pyMarks the folder as a package. Usually empty.
admin.pyRegister models so they appear in the admin site.
apps.pyApp configuration (the AppConfig class).
migrations/Version-controlled database schema changes.
models.pyYour data models β€” one class per table.
tests.pyAutomated tests for the app.
views.pyRequest-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:

graph TD A[blog/] --> B[models.py] A --> C[views.py] A --> D[admin.py] A --> E[urls.py] A --> F[forms.py] A --> G[signals.py] A --> H[templatetags/] A --> I[templates/blog/] A --> J[static/blog/] A --> K[api/]

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: AppConfig is 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 management
  • profiles β€” profile data and settings
  • notifications β€” 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.

PatternUse it when…Coupling
Foreign keys / relationshipsOne app's data genuinely relates to another's.Tight (by design)
SignalsYou want to react to an event without a hard dependency.Loose
Template tagsAn app should offer reusable snippets to any template.Loose
MiddlewareYou 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:

  1. Run startapp blog.
  2. Register it in INSTALLED_APPS using its config class.
  3. Add a Category and Post model (use the code above as a guide).
  4. Register both models in admin.py.
  5. Create blog/urls.py with an app_name and one post_list route, then include() it from the project.
  6. Run makemigrations blog and migrate, 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.
  • startapp scaffolds the app, but you must register it in INSTALLED_APPS for 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

πŸš€ 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.