Project vs. App: A Useful Mental Model
In Django, a project is the top-level container that holds settings, global URL routing, and deployment configuration. An app is a focused, reusable unit that implements a specific backend feature (models, views, URLs, templates, admin, tests). A project can contain many apps; apps can be reused across projects.
Think in “features” and “boundaries”
- Project responsibilities: settings, environment configuration, global middleware, global URL routing, shared infrastructure (logging, storage, auth configuration), and orchestration.
- App responsibilities: one coherent feature area (e.g., billing, inventory, support tickets), owning its models, views, URLs, and tests.
A good rule: if you can describe the app in one sentence and it has a clear set of URLs and data, it’s likely a good app boundary.
How to split backend features into apps
Split by domain (what the business cares about) rather than by technical layers. For example:
accounts: login, user profile, permissions helpersinventory: products, stock movementsorders: carts, checkout, order historyinternal_tools: staff-only dashboards, maintenance endpoints
Avoid creating apps like utils or common too early. Prefer placing small shared helpers in a project-level module (e.g., project_name/common/) until a clear reusable app emerges.
Recommended Project Layout for Maintainable Backends
A common maintainable layout separates the repository root from the Django “config” package and keeps apps in a dedicated folder.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
repo-root/ (git root) project_name/ (python package: config) __init__.py settings/ __init__.py base.py dev.py prod.py urls.py asgi.py wsgi.py apps/ accounts/ __init__.py urls.py views.py models.py tests.py inventory/ __init__.py urls.py views/ __init__.py products.py stock.py models.py tests/ test_products.py manage.pyWhy this layout helps
- Settings split keeps dev/prod differences explicit.
- Apps folder makes feature modules easy to find.
- Views package (a folder) prevents
views.pyfrom becoming a “junk drawer”.
When you use an apps/ folder, remember to reference apps in INSTALLED_APPS with their full dotted path, e.g. project_name.apps.inventory (or whatever your package name is).
URL Routing: From Global URLs to App URLs
Django URL routing is typically layered:
- Project URLConf (global): mounts apps at prefixes using
include(). - App URLConf (local): defines feature-specific routes and names.
Project-level urls.py using include()
Keep the project URLConf small: it should mostly delegate to apps.
from django.contrib import admin from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), path("accounts/", include("project_name.apps.accounts.urls")), path("inventory/", include("project_name.apps.inventory.urls")), path("tools/", include("project_name.apps.internal_tools.urls")), ]This makes it easy to add, remove, or version entire feature areas without editing many files.
Path Converters: Clean, Typed Parameters in Routes
Path converters let you capture parts of the URL and pass them to views with basic validation. Common converters:
<int:id>for integers<slug:slug>for URL-friendly strings<uuid:uuid>for UUIDs<str:name>for any non-slash string
Example: inventory routes with converters
from django.urls import path from . import views app_name = "inventory" urlpatterns = [ path("products/", views.product_list, name="product_list"), path("products/new/", views.product_create, name="product_create"), path("products/<int:pk>/", views.product_detail, name="product_detail"), path("products/<int:pk>/edit/", views.product_update, name="product_update"), path("products/<int:pk>/delete/", views.product_delete, name="product_delete"), ]Using pk is conventional when the view is operating on a model instance. If you use slugs publicly, prefer <slug:slug> for readability.
Custom path converters (when built-ins aren’t enough)
If you have a domain-specific identifier (e.g., INV-2026-000123), define a converter to validate and normalize it.
# inventory/converters.py import re class InventoryCodeConverter: regex = r"INV-\d{4}-\d{6}" def to_python(self, value): return value # could parse into structured parts if needed def to_url(self, value): return str(value)# inventory/urls.py from django.urls import path, register_converter from .converters import InventoryCodeConverter from . import views register_converter(InventoryCodeConverter, "invcode") app_name = "inventory" urlpatterns = [ path("items/<invcode:code>/", views.item_by_code, name="item_by_code"), ]This keeps validation close to routing and prevents invalid identifiers from reaching your view logic.
Namespacing: Prevent URL Name Collisions
As your backend grows, multiple apps may have routes named detail, create, or list. Namespacing ensures URL reversing stays unambiguous.
App-level namespace with app_name
In each app’s urls.py, set app_name and use named routes:
app_name = "accounts" urlpatterns = [ path("profile/", views.profile, name="profile"), ]Then reverse with accounts:profile (in templates or Python).
Instance namespace (mounting the same app twice)
You can mount the same URLConf under different prefixes with different instance namespaces. This is useful for “v1/v2” APIs or multi-tenant admin areas.
# project urls.py from django.urls import include, path urlpatterns = [ path("tools/", include(("project_name.apps.internal_tools.urls", "internal_tools"), namespace="tools")), path("ops/", include(("project_name.apps.internal_tools.urls", "internal_tools"), namespace="ops")), ]Now you can reverse tools:dashboard vs. ops:dashboard even though they share the same underlying URL patterns.
Designing Clean, Maintainable CRUD Routes
For backend pages and internal tools, consistency matters more than cleverness. A predictable CRUD route scheme makes navigation, permissions, and auditing easier.
A practical CRUD pattern
| Action | Route | Name |
|---|---|---|
| List | /products/ | product_list |
| Create | /products/new/ | product_create |
| Detail | /products/<pk>/ | product_detail |
| Update | /products/<pk>/edit/ | product_update |
| Delete | /products/<pk>/delete/ | product_delete |
This avoids ambiguous routes like /products/edit/<pk>/ and keeps the “resource identifier” in one place.
Nested resources (use sparingly)
Sometimes nesting improves clarity, e.g., stock movements belonging to a product:
path("products/<int:product_pk>/stock-movements/", views.stock_movement_list, name="stock_movement_list")Keep nesting shallow. Deep nesting often signals you need a separate identifier (e.g., movement ID) or a filter parameter rather than another URL segment.
Internal tools routing conventions
Internal tools often include dashboards, bulk operations, and maintenance endpoints. Keep them clearly separated from public-facing routes:
- Mount under a distinct prefix like
/tools/or/internal/. - Use explicit verbs for non-CRUD operations:
/tools/cache/clear/,/tools/reindex/search/. - Keep dangerous actions POST-only at the view level (routing stays readable, safety enforced in views).
# internal_tools/urls.py from django.urls import path from . import views app_name = "internal_tools" urlpatterns = [ path("dashboard/", views.dashboard, name="dashboard"), path("cache/clear/", views.clear_cache, name="clear_cache"), path("search/reindex/", views.reindex_search, name="reindex_search"), ]Structuring Views for Clarity (Function-Based First)
As features grow, the main maintainability risk is a single massive views.py. Prefer organizing views by subdomain or resource.
Option A: small app, single views.py
For small apps, a single module is fine:
# inventory/views.py from django.http import HttpResponse def product_list(request): return HttpResponse("...")Option B: larger app, views/ package
For larger apps, split by resource:
inventory/ views/ __init__.py products.py stock.py# inventory/views/products.py from django.http import HttpResponse def product_list(request): return HttpResponse("list") def product_detail(request, pk): return HttpResponse(f"detail {pk}")# inventory/urls.py from django.urls import path from .views import products app_name = "inventory" urlpatterns = [ path("products/", products.product_list, name="product_list"), path("products/<int:pk>/", products.product_detail, name="product_detail"), ]This makes it obvious where to add new endpoints and reduces merge conflicts in teams.
Keep view modules aligned with URL modules
A practical pattern is to mirror URL groupings in your view modules. If you have products/ and stock/ URL sections, keep views/products.py and views/stock.py. When someone reads urls.py, they can immediately find the corresponding view code.
A Brief Look at Class-Based View Patterns (Without Overcommitting)
Function-based views (FBVs) are straightforward and explicit. Class-based views (CBVs) can reduce repetition for CRUD patterns, but they introduce indirection. A balanced approach is to start with FBVs and adopt CBVs when you see repeated patterns (forms, permissions, object lookup).
CBV routing looks similar
# inventory/views/products.py from django.views import View from django.http import HttpResponse class ProductListView(View): def get(self, request): return HttpResponse("list")# inventory/urls.py from django.urls import path from .views.products import ProductListView app_name = "inventory" urlpatterns = [ path("products/", ProductListView.as_view(), name="product_list"), ]Even if you use CBVs, keep the same routing conventions (list/new/detail/edit/delete) so URLs remain predictable.
Keeping Routing Maintainable as the Backend Grows
Group related routes and delegate with include()
If an app’s urls.py becomes long, split it into multiple URL modules and include them.
# inventory/urls.py from django.urls import include, path app_name = "inventory" urlpatterns = [ path("products/", include("project_name.apps.inventory.urls_products")), path("stock/", include("project_name.apps.inventory.urls_stock")), ]# inventory/urls_products.py from django.urls import path from .views import products urlpatterns = [ path("", products.product_list, name="product_list"), path("new/", products.product_create, name="product_create"), path("<int:pk>/", products.product_detail, name="product_detail"), ]This keeps each URL module short and focused while preserving a clean public route structure.
Be consistent with naming
- Use noun-based path segments for resources:
products,orders,tickets. - Use action suffixes for non-idempotent pages:
new,edit,delete. - Use clear route names:
product_updateinstead ofeditto avoid collisions and improve readability.
Reserve the project URLConf for composition
A maintainable project urls.py typically contains:
- Admin route(s)
- App mounts via
include() - Optional health check endpoint (if you use one)
- Optional debug-only routes (guarded by settings)
Everything else should live in the app that owns the feature.