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 app execution as a boundary for a single business domain to minimize coupling.
  • Explicit Configuration: Always subclass AppConfig in apps.py to control initialization, signal registration, and auto-field behavior.
  • Namespace Everything: Use app_name in URL modules and the namespace parameter in include() 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 of AppConfig to ensure once-only registration after model loading.
  • Test Isolation: Keep tests within the app directory and use available_apps for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →