Understanding the Django project vs app distinction
If you've worked with Django for a few weeks, you've likely heard the terms "project" and "app" used interchangeably—and that's where confusion starts. Understanding the Django project vs app: a practical Python Backend guide distinction is fundamental to writing maintainable backend code that scales. At its core, a Django project is a container that holds configuration and routing, while an app is a self-contained feature module that you can theoretically reuse across multiple projects. Getting this right early saves you from refactoring nightmares later.
A project holds settings and URLs
When you run django-admin startproject mysite, Django generates a project directory with a specific structure. The project is your deployment unit—it's what you push to production, what your Docker container runs, what your CI/CD pipeline tests. Inside, you get a settings module (usually settings.py) and a URL configuration file (usually urls.py).
Think of the project as the glue that binds everything together. It defines your database connection, installed apps, middleware, secret keys, allowed hosts, and static file locations. It also defines your top-level URL router—the entry point for all HTTP requests.
Here's what a typical project structure looks like after initial creation:
mysite/
├── mysite/ # Project package
│ ├── __init__.py
│ ├── settings.py # Database, installed apps, secrets
│ ├── urls.py # Top-level URL routing
│ ├── asgi.py # ASGI application (async)
│ └── wsgi.py # WSGI application (sync)
└── manage.py # CLI entry pointIn a real e-commerce backend we've built, the project's urls.py looked like this:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/auth/', include('users.urls')),
path('api/orders/', include('orders.urls')),
path('api/products/', include('catalog.urls')),
]Notice that the project doesn't define individual endpoints—it delegates to apps via include(). The project is a conductor; the apps are the instruments.
Apps are reusable feature modules
An app, created with python manage.py startapp myapp, is a focused Python package that encapsulates a single feature or domain. An app contains models, views, URLs, serializers, permissions, tests, and migrations—everything related to that feature.
The key insight is that apps should be reusable. If you built a users app for one project, you should be able to drop it into another project and have it work (with minimal environment-specific tweaks). This requires discipline: keep business logic within the app, avoid hardcoding project-specific assumptions, and use settings sparingly.
A typical app structure:
users/
├── migrations/
│ ├── __init__.py
│ └── 0001_initial.py
├── __init__.py
├── admin.py # Admin panel registration
├── apps.py # App configuration
├── models.py # Database models
├── views.py # Business logic / endpoints
├── urls.py # App-level URL routing
├── serializers.py # Data serialization (DRF)
├── permissions.py # Custom permission classes
├── tests.py # Unit and integration tests
└── utils.py # App-specific utilitiesIn a ride-hailing backend, we had three core apps: drivers, riders, and trips. Each was independently testable and could theoretically be spun into a microservice later. The drivers app handled driver registration, license verification, and rating. The riders app handled user onboarding and payment methods. The trips app orchestrated the matching and tracking logic.
Here's what the drivers app's urls.py looked like:
from django.urls import path
from . import views
app_name = 'drivers'
urlpatterns = [
path('register/', views.DriverRegisterView.as_view(), name='register'),
path('<int:id>/', views.DriverDetailView.as_view(), name='detail'),
path('<int:id>/documents/', views.DriverDocumentsView.as_view(), name='documents'),
path('<int:id>/ratings/', views.DriverRatingsView.as_view(), name='ratings'),
]Notice the app_name declaration. This namespaces the URLs, preventing collisions if multiple apps have similar endpoint names.
Keep apps focused: users, billing, catalog
The biggest mistake we see is apps that try to do too much. We've inherited codebases with a core app that had 47 models and touched every domain. That's not an app—that's a dumping ground.
A well-designed app should answer this question clearly: "What is this app's job?" If you struggle to answer in one sentence, the app is too broad.
For an SaaS platform we built, we kept these boundaries strict:
- accounts: User registration, login, password reset, profile updates.
- subscriptions: Billing plans, invoices, payment processing, seat management.
- workspaces: Workspace creation, permissions, team member management.
- features: The core product logic specific to the SaaS offering.
- audit: Logging and compliance across all apps.
Could you combine accounts and subscriptions? Technically yes, but then changing billing doesn't require deploying account logic, and vice versa. Separation reduces test scope, deployment risk, and cognitive load.
A billing app model might look like:
from django.db import models
from accounts.models import Account
class Subscription(models.Model):
PLAN_CHOICES = [
('starter', 'Starter - $29/mo'),
('pro', 'Pro - $99/mo'),
('enterprise', 'Enterprise - Custom'),
]
account = models.OneToOneField(Account, on_delete=models.CASCADE)
plan = models.CharField(max_length=20, choices=PLAN_CHOICES)
billing_date = models.DateField()
status = models.CharField(max_length=20, default='active')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'billing'Notice we import from another app (accounts.models.Account). That's fine—apps talk to each other. But the Subscription model belongs in billing, not in a tangled core module.
Register apps in INSTALLED_APPS
For Django to recognize an app, you must register it in your project's settings.py under the INSTALLED_APPS list. Without this, migrations won't run, models won't be found, and admin registration fails silently.
When you run python manage.py startapp myapp, Django creates an apps.py` file in the app directory. This file contains the app's configuration class:
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'
verbose_name = 'User Management'Then in your project's settings.py, you register the app:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'rest_framework',
'corsheaders',
# Your apps
'users.apps.UsersConfig',
'billing.apps.BillingConfig',
'catalog.apps.CatalogConfig',
]The order matters for inheritance and signal handling. Third-party apps typically come before your apps, and within your apps, put foundational ones (like users) before dependent ones.
A common pattern in production is to keep environment-specific app registration in separate settings files:
# settings/base.py
INSTALLED_APPS = [
'django.contrib.admin',
# ... core apps
]
# settings/development.py
from .base import *
INSTALLED_APPS += [
'django_extensions',
'debug_toolbar',
]Common mistakes
Mistake 1: Creating too many small apps. We've seen projects with 30+ apps, each with 2-3 models. This fragments code and creates import hell. A good rule of thumb: one app per major feature or domain, not one per model.
Mistake 2: Mixing project and app concerns. Your project's urls.py should not contain business logic views. It should only orchestrate includes. If you find yourself writing complex view code in urls.py, move it to an app.
Mistake 3: Circular imports between apps. If users.models imports from orders.models and orders.models imports from users.models, you're headed for trouble. Use lazy imports or abstract your shared logic into a third app.
Mistake 4: Forgetting to add apps to INSTALLED_APPS. We've debugged this countless times in new projects. A model won't be created in the database, migrations won't run, and the error messages are cryptic. Always register your app immediately after creating it.
Mistake 5: Assuming app reusability without actually testing it. Just because an app is theoretically reusable doesn't mean it is. Test this: can you pip install it in another project and have it work with minimal glue code? If not, you've hardcoded project assumptions.
Key takeaways
- A Django project is a deployment unit containing settings, secrets, and top-level URL routing. A Django app is a reusable feature module containing models, views, URLs, and tests.
- Keep each app focused on a single domain or feature. If you can't describe an app's purpose in one sentence, it's too broad.
- Register every app in
INSTALLED_APPSwith itsAppConfigclass. Order matters: third-party first, then foundational apps, then dependent apps. - Use app-level
urls.pyfiles withinclude()and namespacing. Your project'surls.pyshould be a dispatcher, not a business logic layer. - Apps should be reusable across projects. If you find yourself hardcoding project paths or assumptions, refactor them into settings or utils.






Comments
Loading comments…