Best Practices for Structuring a Django Project Like a Pro!

Follow these best practices to structure your Django project like a pro — clean, maintainable, and future-proof!

Best Practices for Structuring a Django Project Like a Pro!
Photo by Cytonn Photography on Unsplash

Build scalable Django apps with ease!

Best Practices for Structuring a Django Project Like a Pro!

In this guide, we’ll walk through best practices for structuring a Django project like a pro, ensuring that your application remains clean, efficient, and easy to manage.


Checkout this article to structuring a python like a pro
Best Practices for Structuring a Python Project Like a Pro! 🚀
Follow these best practices to structure your Python projects like a pro – clean, scalable, and maintainable!

1. Use a Modular Project Structure

By default, Django generates a basic structure when you run django-admin startproject myproject. While this works for small applications, it's not ideal for large-scale projects. Instead, follow a modular approach by dividing your app into separate functional components.

myproject/ 
│── manage.py 
│── pyproject.toml         # Manage dependencies 
│── config/                # Configuration settings 
│   ├── __init__.py 
│   ├── settings/ 
│   │   ├── __init__.py 
│   │   ├── base.py 
│   │   ├── development.py 
│   │   ├── production.py 
│   ├── urls.py 
│   ├── wsgi.py 
│   ├── asgi.py 
│── apps/ 
│   ├── users/ 
│   │   ├── migrations/ 
│   │   ├── templates/ 
│   │   ├── views.py 
│   │   ├── models.py 
│   │   ├── urls.py 
│   │   ├── serializers.py 
│   │   ├── forms.py 
│   │   ├── admin.py 
│   │   ├── tests.py 
│   │   ├── signals.py 
│   ├── blog/ 
│   ├── payments/ 
│── templates/              # Global templates 
│── static/                 # Global static files 
│── media/                  # Uploaded files 
│── scripts/                # Management and utility scripts

Why This Structure?

  • Encapsulates each app’s functionality (e.g., users, blog, payments).
  • Separates configuration files for different environments.
  • Keeps templates and static files organized at the project level.

2. Keep Settings Organized

Managing Django settings in a single settings.py file can become chaotic as your project grows. Instead, split the settings into multiple files:

config/settings/ 
│── __init__.py 
│── base.py          # Common settings 
│── development.py   # Dev-specific settings 
│── production.py    # Production-specific settings

How to Use It?

Modify config/settings/__init__.py to dynamically load the correct settings:

import os 
 
ENVIRONMENT = os.getenv("DJANGO_ENV", "development") 
 
if ENVIRONMENT == "production": 
    from .production import * 
else: 
    from .development import *

This approach ensures that your settings remain clean and modular while preventing accidental use of development settings in production.

Checkout this article to setup settings for different environments:
Master Django Settings Like a Pro with Code Splitting! 🚀
Managing settings in a Django project can get messy easily, especially as your project scales. A single settings.py…

3. Use a Dedicated Configuration Directory

Keeping configuration files separate from your core Django apps makes management easier. Store all environment-specific settings, logging configurations, and external service credentials in a dedicated config/ directory.

Example:

config/ 
│── settings/ 
│── logging.py 
│── celery.py 
│── urls.py 
│── wsgi.py 
│── asgi.py

4. Use Environment Variables for Secrets

Never hardcode sensitive information like database credentials or API keys in your settings files. Instead, use environment variables and load them securely with python-dotenv.

Example:

import os 
from dotenv import load_dotenv 
 
load_dotenv() 
 
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY") 
DATABASE_URL = os.getenv("DATABASE_URL")

Place your .env file in the root directory:

DJANGO_SECRET_KEY=your-secret-key 
DATABASE_URL=postgres://user:password@localhost/dbname

And ensure it is excluded from version control by adding it to .gitignore.

5. Separate Business Logic from Views

Django views should be kept lean and focused on handling HTTP requests. Business logic should be placed in:

  • Services Layer (services.py) for core application logic.
  • Managers (models.py) for custom database queries.
  • Utils (utils.py) for reusable helper functions.

Example of a Service Layer (services.py):

# services.py 
from django.core.mail import send_mail 
 
def send_welcome_email(user): 
    send_mail( 
        "Welcome!", 
        "Thank you for signing up.", 
        "noreply@myapp.com", 
        [user.email], 
    )

Then, in the view:

from .services import send_welcome_email 
 
def register_user(request): 
    user = User.objects.create(...) 
    send_welcome_email(user)

This approach keeps your views clean and business logic reusable.

6. Use Django Signals for Decoupling

Instead of tightly coupling logic within views or models, use Django’s signals to handle side effects like sending notifications or updating related models.

Example (signals.py in users app):

from django.db.models.signals import post_save 
from django.dispatch import receiver 
from .models import UserProfile, User 
 
@receiver(post_save, sender=User) 
def create_user_profile(sender, instance, created, **kwargs): 
    if created: 
        UserProfile.objects.create(user=instance)

And in apps.py:

def ready(self): 
    import users.signals

This makes your code more modular and maintainable.

Checkout this article to learn more about signals:
Django Signals: The Hidden Power Behind Scalable Applications
Django signals are one of the most powerful yet often underutilized features of Django. They allow different parts of…

7. Use Class-Based Views (CBVs) for Scalability

Function-based views (FBVs) are fine for small applications, but class-based views (CBVs) are more scalable and reusable.

Example of a CBV:

from django.views.generic import ListView 
from .models import BlogPost 
 
class BlogPostListView(ListView): 
    model = BlogPost 
    template_name = "blog/post_list.html"

This approach provides built-in methods for CRUD operations and keeps views clean.

8. Implement Proper Testing

Testing is essential for ensuring your application works as expected. Use Django’s built-in unittest framework and organize tests into separate files.

Example (tests/test_models.py):

from django.test import TestCase 
from .models import User 
 
class UserModelTest(TestCase): 
    def test_user_creation(self): 
        user = User.objects.create(username="testuser") 
        self.assertEqual(user.username, "testuser")

Run tests using:

python manage.py test

Always maintain 100% test coverage for critical business logic.

9. Use UV Modern Project Management Tool

uv is an ultra-fast Python package and project manager written in Rust. It aims to be a drop-in replacement for pip, pip-tools, venv, and virtualenv, providing significant performance improvements while maintaining compatibility with existing Python package management workflows.

Installation

pip install uv

Example

Let’s create a project with uv:

$ uv init --lib demo

This will create this project structure:

demo 
  ├── README.md 
  ├── pyproject.toml 
  └── src 
      └── demo 
          ├── __init__.py 
          └── py.typed

Add dependency:

$ uv add django

10. Use Ruff for fast Linter & Formatter

Ruff is a blazing-fast linter written in Rust, designed to replace Flake8, Black, and isort in one tool.

Installation:

pip install ruff

Example

Let’s use uv to initialize a project:

uv init --lib demo

This command creates a Python project with the following structure:

demo 
  ├── README.md 
  ├── pyproject.toml 
  └── src 
      └── demo 
          ├── __init__.py 
          └── py.typed

We’ll then replace the contents of src/demo/__init__.py with the following code:

import os 
from typing import Iterable 
 
def sum_even_numbers(numbers: Iterable[int]) -> int: 
    """Given an iterable of integers, return the sum of all even numbers in the iterable.""" 
    return sum( 
        num for num in numbers 
        if num % 2 == 0 
    )

Next, we’ll add Ruff to our project:

uv add --dev ruff

We can then run the Ruff linter over our project via uv run ruff check:

$ uv run ruff check 
src/numbers/__init__.py:3:8: F401 [*] `os` imported but unused 
Found 1 error. 
[*] 1 fixable with the `--fix` option.

we can resolve the issue automatically by running ruff check --fix:

$ uv run ruff check --fix 
Found 1 error (1 fixed, 0 remaining).

11. Use Docker for Environment Consistency

Containerizing your Django app with Docker ensures consistency across development and production environments.

Example Dockerfile:

FROM python:3.12 
 
WORKDIR /app 
COPY . . 
RUN pip install -r requirements.txt 
 
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"]

And a docker-compose.yml file for services like PostgreSQL.


Checkout this article to learn about the Django best practices you should always follow!
Django Development: 14 Best Practices You Should Always Follow!
Follow these essential Django development tips to write cleaner, faster, and more maintainable code.

Final Thoughts

By following these best practices, you ensure that your project stays clean, efficient, and professional.

Do you have other Django structuring tips? Share them in the comments below!


Photo by Martin Katler on Unsplash