Django Create New App: Best Practices for Scalable Sub-Application Architecture
When you run django-admin startapp inside an existing project, you should treat each new app as a self-contained, reusable module with isolated URLs, templates, and business logic to ensure long-term maintainability.
Creating a new Django app within an existing project is more than just running a command—it is an architectural decision that impacts how your codebase scales. According to the Django source code in django/core/management/commands/startapp.py, the scaffold generated by startapp provides only a skeleton; the responsibility for structuring domain isolation, namespace routing, and signal registration falls to the developer. Following the conventions established in docs/intro/reusable-apps.txt and demonstrated by Django’s own contrib apps (such as django/contrib/auth), you can build sub-applications that remain decoupled, testable, and portable.
Core Principles for Django App Architecture
Single Responsibility and Domain Isolation
Each app created via django create new app should encapsulate a single business domain—such as users, payments, or inventory. This mirrors the philosophy outlined in docs/intro/reusable-apps.txt, which emphasizes that reusable apps are self-contained and do not reach into unrelated parts of the project. By isolating models, views, and templates within the app’s directory, you reduce coupling and make it possible to extract the app into a standalone package later.
The AppConfig Foundation
Every new app should provide an explicit AppConfig subclass in apps.py. As documented in docs/ref/applications.txt, this configuration class is the hook for application-specific initialization, including signal registration and the setting of default_auto_field.
# users/apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'
def ready(self):
# Import signals to ensure they are registered
import users.signals
In settings.py, reference the config class directly in INSTALLED_APPS rather than just the app name:
# myproject/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'users.apps.UsersConfig', # Explicit AppConfig
'blog.apps.BlogConfig',
]
URL Routing and Namespace Isolation
Implementing App-Level URL Namespacing
To prevent URL name collisions across multiple apps, Django supports URL namespacing via the app_name variable and the namespace parameter in include(). This pattern is detailed in docs/topics/http/urls.txt.
Define your app’s URLs with an explicit app_name:
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.PostListView.as_view(), name='list'),
path('<int:pk>/', views.PostDetailView.as_view(), name='detail'),
]
Then include the app’s URLs in the project root with a matching namespace:
# myproject/urls.py
from django.urls import include, path
urlpatterns = [
path('blog/', include('blog.urls', namespace='blog')),
path('users/', include('users.urls', namespace='users')),
]
This allows you to use unambiguous reverse URL lookups such as {% url 'blog:detail' post.pk %} in templates or reverse('blog:list') in Python code.
Static Assets and Template Organization
Template Directory Conventions
Django’s template loader searches for templates in app directories, but to avoid naming conflicts, you should place templates within a subdirectory named after the app. As noted in docs/topics/templates.txt, the convention is <app>/templates/<app>/template_name.html.
blog/
templates/
blog/
post_list.html
post_detail.html
This ensures that post_list.html from the blog app does not collide with a template of the same name in the news app.
Static File Scoping
Similarly, static files should be scoped under <app>/static/<app>/ to prevent CSS or JavaScript collisions:
blog/
static/
blog/
css/
style.css
js/
comments.js
Reference these in templates using the app-prefixed path:
{% load static %}
<link rel="stylesheet" href="{% static 'blog/css/style.css' %}">
Data Layer and Business Logic
Model Inheritance Strategies
When multiple apps share common fields (such as created_at and updated_at timestamps), use abstract base models to keep code DRY while maintaining independent migration histories for each app. As documented in docs/ref/models/fields.txt, abstract models are not created in the database but serve as base classes.
# common/models.py
from django.db import models
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
# blog/models.py
from django.db import models
from common.models import TimeStampedModel
class Post(TimeStampedModel):
title = models.CharField(max_length=200)
content = models.TextField()
This approach ensures that the blog app’s migrations remain independent of other apps using the same abstract base.
Signal Registration Patterns
Django signals (such as post_save or user_logged_in) should be registered inside the ready() method of your AppConfig to ensure they are connected exactly once and only after all models are loaded. This pattern is detailed in docs/topics/signals.txt.
# users/signals.py
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
@receiver(user_logged_in)
def track_user_login(sender, request, user, **kwargs):
# Implementation for audit logging
pass
# users/apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
def ready(self):
import users.signals # noqa: F401
Using the ready() method prevents circular import issues and ensures signal handlers are not registered multiple times during testing or when using development auto-reloaders.
Testing and Operations
Isolated Test Suites
Tests for each app should reside in <app>/tests/ or a top-level tests/ package with clear module separation. As documented in docs/topics/testing/advanced.txt, when building reusable apps, use available_apps or isolate_apps to prevent test side-effects from leaking between applications.
# blog/tests/test_models.py
from django.test import TestCase
from blog.models import Post
class PostModelTests(TestCase):
def test_post_creation(self):
post = Post.objects.create(title="Test", content="Body")
self.assertEqual(str(post), "Test")
For advanced scenarios involving reusable apps, Django’s test runner supports isolation attributes:
from django.test import TestCase
class IsolatedAppTests(TestCase):
available_apps = ['blog'] # Only load this app for speed and isolation
Management Commands and Migrations
Custom management commands should be placed in <app>/management/commands/ to keep administrative tooling close to the domain logic it manipulates, as referenced in docs/ref/custom-management-commands.txt.
blog/
management/
commands/
publish_scheduled_posts.py
Migrations must remain within the app’s <app>/migrations/ directory. Running makemigrations per app (e.g., python manage.py makemigrations blog) rather than globally helps maintain independent schema evolution paths and simplifies rollbacks, as detailed in docs/topics/migrations.txt.
Summary
- Domain Isolation: Treat each
django create new appexecution as a boundary for a single business domain to minimize coupling. - Explicit Configuration: Always subclass
AppConfiginapps.pyto control initialization, signal registration, and auto-field behavior. - Namespace Everything: Use
app_namein URL modules and thenamespaceparameter ininclude()to prevent reverse URL collisions. - Asset Scoping: Place templates under
<app>/templates/<app>/and static files under<app>/static/<app>/to avoid cross-app file conflicts. - Signal Safety: Register signal handlers inside the
ready()method ofAppConfigto ensure once-only registration after model loading. - Test Isolation: Keep tests within the app directory and use
available_appsfor reusable app testing to prevent side effects.
Frequently Asked Questions
What is the difference between a Django project and a Django app?
A Django project is the entire web application instance, defined by the settings.py file and the root URL configuration, while a Django app is a self-contained module that performs a specific function, such as a blog or payment system. According to docs/intro/reusable-apps.txt, apps are designed to be reusable across projects, whereas the project itself is the specific configuration that ties apps together.
Should I use relative or absolute imports within a Django app?
You should use absolute imports (e.g., from users.models import Profile) rather than relative imports inside Django apps. Absolute imports improve readability, prevent import-time side effects, and make it easier to move or refactor apps later. This aligns with general Python best practices and the patterns observed in django/contrib apps.
Where should I register Django signals to avoid circular imports?
Register Django signals inside the ready() method of your AppConfig subclass in apps.py. As documented in docs/topics/signals.txt, this method is executed after all models are loaded, ensuring that signal handlers can safely import models without causing circular import errors. Import the signals module inside the ready() method rather than at the top of the file to delay the import until Django is fully initialized.
How do I make a Django app reusable for other projects?
To make a Django app reusable, ensure it has no hard dependencies on the specific project settings, uses explicit AppConfig subclasses, namespaces its URLs, and scopes templates and static files under an app-specific subdirectory. Include a README.rst documenting settings and dependencies, and follow the testing isolation patterns described in docs/topics/testing/advanced.txt using available_apps. The app should function as a standalone Python package that can be dropped into any Django project’s INSTALLED_APPS.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →