Free Ebook cover Django Fundamentals: From First App to a Complete Backend

Django Fundamentals: From First App to a Complete Backend

New course

12 pages

Django Project Structure, Apps, and URL Routing for Backend Features

Capítulo 2

Estimated reading time: 9 minutes

+ Exercise

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 helpers
  • inventory: products, stock movements
  • orders: carts, checkout, order history
  • internal_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 App

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.py

Why this layout helps

  • Settings split keeps dev/prod differences explicit.
  • Apps folder makes feature modules easy to find.
  • Views package (a folder) prevents views.py from 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

ActionRouteName
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_update instead of edit to 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.

Now answer the exercise about the content:

In a growing Django backend, which approach best keeps global URL routing maintainable while allowing each feature to own its own routes?

You are right! Congratulations, now go to the next page

You missed! Try again.

The project URLConf should mainly compose the site by mounting apps with include(). Each app then owns its feature routes, making it easier to add/remove whole feature areas and avoid a bloated global urls.py.

Next chapter

Django Models and Database Design with the ORM

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.