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!

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

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.
Recommended Structure:
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:

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:

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!

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!
