Jak zacząć z Django: struktura projektu i dobre praktyki

0
90
Rate this post

Nawigacja:

Start z Django – minimalny kontekst i wymagania wstępne

Cel na początek jest prosty: zbudować pierwszy działający projekt w Django, zrozumieć jego strukturę i nie wkopać się w typowe pułapki, które później trudno odkręcić. Prawidłowy start procentuje przy każdej kolejnej aplikacji.

Kiedy Django ma sens, a kiedy lepiej je odpuścić

Django jest frameworkiem wysokiego poziomu do aplikacji webowych w Pythonie. Najlepiej sprawdza się w projektach, w których:

  • potrzebny jest panel administracyjny (CRUD na modelach, szybkie formularze),
  • logika biznesowa jest dość rozbudowana (relacje w bazie, autoryzacja, sesje, formularze),
  • ma być jedna większa aplikacja, którą rozwija się przez lata (np. CRM, panel klienta, intranet, serwis ogłoszeniowy),
  • zależy na spójności – jeden sposób robienia rzeczy, dużo wbudowanych narzędzi.

Nie jest to najlepszy wybór, gdy:

  • tworzysz prosty, jednofunkcyjny mikroserwis HTTP, który tylko przyjmuje JSON i zwraca JSON – wówczas wygodniejszy bywa FastAPI lub Flask,
  • masz ekstremalne wymagania wydajnościowe na poziomie niskopoziomowego I/O – wtedy Python web może być wąskim gardłem niezależnie od frameworka,
  • projekt to wyłącznie front-end SPA z minimalnym backendem – wtedy Django bywa „za grube”.

Dla większości typowych paneli, aplikacji biznesowych, systemów zarządzania treścią czy platform z użytkownikami i płatnościami, Django jest bardzo rozsądnym wyborem na start.

Minimalne wymagania: co trzeba umieć przed Django

Django rozwiązuje dużo problemów, ale nie zastąpi podstaw. Przed wejściem w strukturę projektu dobrze mieć minimum:

  • Python – składnia, funkcje, klasy, moduły, podstawy pakietów. Umiejętność czytania błędów (traceback).
  • HTTP – co to jest request, response, metody GET/POST, statusy 200/404/500, nagłówki, cookies.
  • Bazy danych – przynajmniej ogólne rozumienie tabel, kluczy głównych i obcych, podstawowe SELECT/INSERT.
  • CLI – poruszanie się po terminalu, uruchamianie poleceń, praca w katalogach.
  • Wirtualne środowiska – idea izolowania zależności per projekt.

Bez tego da się „odpalić” Django, ale trudno będzie świadomie projektować strukturę i dobre praktyki. Krótkie odświeżenie Pythona i baz danych mocno przyspiesza postęp.

Django, Flask, FastAPI – dlaczego tu wygrywa Django

Trzy częste opcje w świecie Pythona:

FrameworkPoziomGłówne zastosowanie
DjangoWysoki (batteries included)Pełne aplikacje webowe, panele, systemy z logowaniem
FlaskNiski / średniMałe serwisy, API, gdy chcesz dobierać biblioteki samodzielnie
FastAPIŚredniNowoczesne API, mocne wsparcie dla typów, OpenAPI, async

Na start Django daje największy „efekt wow” – w kilka minut masz:

  • gotowy panel admina,
  • ORM do pracy z bazą,
  • system szablonów,
  • autoryzację, sesje i formularze.

Dlatego przy pierwszych większych projektach skupienie się na Django ma sens – uczy wzorców, które potem przenosisz także do innych frameworków.

Sensowny cel pierwszego projektu w Django

Najlepiej ustawić sobie konkretny, mały, ale „prawdziwy” cel, np.:

  • prosty serwis ogłoszeniowy z użytkownikami i formularzem dodawania ogłoszeń,
  • klasyczny blog z kategoriami, komentarzami i panelem admina,
  • wewnętrzny mini-CRM dla małego zespołu (kontakty, notatki, zadania).

Projekt powinien wymagać przynajmniej:

  • kilku modeli i relacji,
  • formularzy (ModelForm),
  • autoryzacji (logowanie, uprawnienia),
  • kilku aplikacji Django w jednym projekcie.

Przy takim zakresie w praktyce poczujesz strukturę projektu Django, przepływ żądania i organizację kodu. Do samego ćwiczenia struktury wystarczy nawet mały „panel z listą zadań”, o ile zrobisz go świadomie z podziałem na aplikacje.

Zbliżenie ekranu z kodem w Pythonie w edytorze podczas pracy nad Django
Źródło: Pexels | Autor: Simon Petereit

Instalacja, virtualenv i pierwszy projekt – stabilny punkt wyjścia

Wybór wersji Pythona i Django

Django ma jasno zdefiniowane wymagania co do wersji Pythona. Przed startem sprawdź:

  • jaką wersję Pythona masz zainstalowaną: python --version lub python3 --version,
  • jaka wersja Django jest aktualnie wspierana i jakie ma wymagania (dokumentacja „Supported Versions”).

Dla pierwszych projektów rozsądnie trzymać się stabilnego wydania Django, najlepiej z dłuższym wsparciem (LTS). Daje to pewność, że:

  • dokumentacja będzie aktualna przez kilka lat,
  • większość tutoriali będzie pasować wersjami,
  • aktualizacje nie będą zaskakiwać co kilka miesięcy.

Kwestia systemu operacyjnego:

  • Windows – wszystko działa, ale czasem trafiają się drobne różnice w ścieżkach i poleceniach. Warto konsekwentnie używać python i py -m venv.
  • macOS / Linux – wygodniej, bo większość instrukcji jest pisana właśnie pod te systemy. Zwykle używasz python3 i python3 -m venv.

Najważniejsze, by w dokumentacji sprawdzić: „Django X.Y supports Python A.B, A.C” i dobrać parę, która się pokrywa.

Tworzenie i używanie wirtualnych środowisk

Wirtualne środowisko izoluje zależności między projektami. Dzięki temu jeden projekt może mieć Django 4.x, inny 5.x i nic się nie gryzie. Na start wystarczy wbudowany venv.

venv vs pipenv vs poetry na start

Krótkie porównanie sposobów zarządzania zależnościami:

  • venv + pip – najprostsze, wbudowane w Pythona, pełna kontrola, dobry wybór na początek.
  • pipenv – wprowadza Pipfile i lockfile, wygodny dla części projektów, ale ma swój narzut.
  • poetry – potężny menedżer zależności i publikacji paczek, świetny, ale dodaje sporo koncepcji.

Na pierwszych projektach Django trzymaj się venv + pip. Mniej narzędzi = mniej problemów.

Prosta checklista tworzenia projektu z venv

Minimalny, powtarzalny zestaw kroków:

  • Utwórz katalog na projekt: mkdir moj_projekt && cd moj_projekt.
  • Utwórz wirtualne środowisko:
    • Linux/macOS: python3 -m venv venv
    • Windows: py -m venv venv
  • Aktywuj środowisko:
    • Linux/macOS: source venv/bin/activate
    • Windows (cmd): venvScriptsactivate
  • Zainstaluj Django: pip install "Django<=X.Y" (konkretną wersję dostosuj).
  • Zapisz zależności: pip freeze > requirements.txt.

Od tej pory w tym katalogu pracujesz tylko z aktywnym venv. Wyłączasz go komendą deactivate.

django-admin startproject w praktyce

Po zainstalowaniu Django dostępne jest narzędzie django-admin. Pozwala utworzyć szkielet projektu:

django-admin startproject config .

Na końcu kropka oznacza „tu, w bieżącym katalogu”. Struktura po tym poleceniu może wyglądać tak:

  • manage.py
  • config/
    • __init__.py
    • asgi.py
    • settings.py
    • urls.py
    • wsgi.py
  • venv/ (jeśli tworzysz środowisko w tym samym katalogu)
  • requirements.txt

Po starcie projektu uruchamiasz serwer deweloperski:

python manage.py runserver

Następnie wchodzisz w przeglądarce na http://127.0.0.1:8000/. Widok strony startowej Django oznacza, że projekt działa.

Różnica między katalogiem głównym a modułem projektu

Często nazwa katalogu projektu jest powtórzona, np. mysite/mysite/settings.py. Pierwszy poziom to katalog na repozytorium (kod, venv, pliki konfiguracyjne), drugi to moduł Pythona zawierający właściwy projekt Django.

Dobrą praktyką jest nazywanie modułu konfiguracyjnego czymś w stylu config, żeby uniknąć chaosu, np.:

  • katalog repo: crm/,
  • moduł projektu: config/,
  • aplikacje: customers/, invoices/, itd.

Dzięki temu w importach od razu widzisz, czy odwołujesz się do głównej konfiguracji (config.settings), czy do konkretnej aplikacji.

Model MVT i jak to się przekłada na strukturę projektu

MVC vs MVT – co to zmienia w praktyce

Django używa wzorca MVT (Model – View – Template), który jest bliski klasycznemu MVC, ale elementy są inaczej nazwane:

  • Model – odpowiednik „M” z MVC, opisuje dane i logikę z nimi związaną (klasy dziedziczące z models.Model).
  • View – w Django to funkcja lub klasa, która przyjmuje request i zwraca response. Najczęściej zawiera logikę biznesową i wybór szablonu.
  • Template – odpowiada za warstwę prezentacji (HTML, czasem JSON generowany na bazie danych kontekstowych).

Routing (w MVC zwykle w kontrolerze) jest w Django wyciągnięty do urls.py. Część osób traktuje urls.py jako „Controller”, a views.py jako coś pomiędzy Controller a View z MVC. Ważne jest, by utrzymać prostą zasadę:

  • modele – dane i logika domenowa,
  • widoki – spinają request, modele i szablony,
  • szablony – minimalna logika, głównie prezentacja.

Przepływ żądania w Django – od URL do odpowiedzi

Typowy scenariusz:

  1. Przeglądarka wywołuje GET /posts/.
  2. Django szuka dopasowania w urls.py – najpierw projektu, potem włączonych plików urls.py z aplikacji.
  3. Po znalezieniu ścieżki, wywoływana jest przypisana funkcja (lub klasa) widoku.
  4. Widok:
    • pobiera dane z modeli (np. Post.objects.all()),
    • dodaje logikę (filtrowanie, paginację),
    • przekazuje dane do szablonu przez render lub generuje JSON.
  5. Szablon generuje HTML (lub inne dane) i Django odsyła je jako HTTP Response.

Prosty przykład endpointu zwracającego listę postów w HTML:

# blog/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("posts/", views.post_list, name="post_list"),
]
# blog/views.py
from django.shortcuts import render
from .models import Post

def post_list(request):
    posts = Post.objects.all().order_by("-created_at")
    return render(request, "blog/post_list.html", {"posts": posts})

Szablon w templates/blog/post_list.html renderuje listę:

<h1>Posty</h1>
<ul>
  {% for post in posts %}
    <li>{{ post.title }}</li>
  {% endfor %}
</ul>

Jak myśleć o projekcie na poziomie architektury

Myślenie aplikacjami, nie „warstwami”

Najczęstszy błąd na starcie to próba dzielenia projektu według warstw: jedna aplikacja models, druga views, trzecia api. Django promuje inny podział – według obszarów domeny. Przekłada się to bezpośrednio na strukturę projektu i łatwość utrzymania.

Przykład sensownego podziału dla prostego CRM:

  • customers/ – klienci, ich dane, segmentacja,
  • deals/ – szanse sprzedażowe, pipeline,
  • activities/ – spotkania, telefony, notatki.

Każda aplikacja ma swoje modele, widoki, szablony i URL-e. Dzięki temu:

  • łatwo wyszukać kod związany z konkretną funkcją biznesową,
  • można niezależnie testować i modyfikować moduły,
  • łatwiej wydzielić aplikację do innego projektu jako paczkę.

Warstwy (dostęp do danych, API, logika domenowa) pojawiają się głównie w strukturze wewnątrz aplikacji – nie w nazwach aplikacji.

Zbliżenie ekranu z kodem Ruby on Rails w edytorze programistycznym
Źródło: Pexels | Autor: Digital Buggu

Struktura projektu po starcie – co oznacza każdy plik i katalog

Plik manage.py – co faktycznie robi

manage.py to cienka otoczka wokół narzędzi Django. Najczęściej używane komendy w kontekście struktury projektu:

  • python manage.py runserver – lokalny serwer developerski,
  • python manage.py startapp blog – tworzenie nowych aplikacji,
  • python manage.py makemigrations – generowanie migracji z modeli,
  • python manage.py migrate – zastosowanie migracji do bazy,
  • python manage.py createsuperuser – konto do panelu admina.

Z perspektywy struktury ważne jest jedno: uruchamiasz wszystko przez manage.py. Ten plik ustawia zmienną środowiskową DJANGO_SETTINGS_MODULE i dba o to, żeby Django znało konfigurację projektu.

Katalog projektu (config/) – serce konfiguracji

W module projektu (np. config/) leżą pliki, które sterują „ramą” całej aplikacji.

  • __init__.py – oznacza, że katalog jest modułem Pythona. Tu czasem dodaje się drobne hacki (np. konfigurację Celery), ale lepiej trzymać go raczej pustego.
  • settings.py – globalna konfiguracja projektu: bazy danych, aplikacje, middleware, ścieżki do plików statycznych i szablonów, ustawienia bezpieczeństwa.
  • urls.py – główny router projektu; włącza URL-e aplikacji.
  • wsgi.py – punkt wejścia dla serwerów WSGI (Gunicorn, uWSGI). Używany przy deploymentcie klasycznym.
  • asgi.py – punkt wejścia dla ASGI (Daphne, Uvicorn). Potrzebny, jeśli wchodzisz w WebSockety, async, Django Channels lub nowoczesne serwery.

Ten moduł ma trzymać tylko to, co jest wspólne dla całego projektu: konfigurację, centralny routing, inicjalizację.

Gdzie trzymać statyczne pliki i szablony

Po starcie projektu katalogi na szablony i statyki nie pojawiają się automatycznie. Trzeba je dodać samemu. Sensowny układ dla prostego projektu:

  • templates/ – globalne szablony (np. wspólny layout, strony błędów),
  • static/ – globalne zasoby (CSS, JS, wspólne obrazy),
  • w każdej aplikacji:
    • app_name/templates/app_name/ – szablony tej aplikacji,
    • app_name/static/app_name/ – statyki tej aplikacji.

Dla aplikacji blog możesz mieć:

blog/
    templates/
        blog/
            post_list.html
            post_detail.html
    static/
        blog/
            blog.css

Taki układ zapobiega konfliktom nazw (np. kilka plików post_list.html z różnych aplikacji) i pozwala łatwo przenosić aplikacje między projektami.

Rozszerzanie struktury aplikacji powyżej „gołego startapp”

Domyślna struktura po startapp jest dobra na dzień 1, ale szybko robi się ciasno, jeśli wszystko wrzucasz do jednego views.py. Przy projektach, które mają przetrwać dłużej niż jeden weekend, przydaje się delikatne rozbicie.

Minimalny, praktyczny podział w aplikacji:

  • models.py – modele domenowe,
  • views/ (katalog):
    • __init__.py
    • public.py – widoki publiczne,
    • admin.py – widoki panelu wewnętrznego lub dedykowane,
    • api.py – endpointy API (jeśli jeszcze nie używasz DRF).
  • urls.py – adresy URL dla tej aplikacji,
  • forms.py – formularze Django,
  • services.py lub logic.py – „goła” logika biznesowa niewiązana sztywno z widokiem,
  • selectors.py lub repositories.py – zapytania do bazy (często wyciągane z widoków).

Ten podział nie jest obowiązkowy, ale szybko porządkuje projekt. Widok staje się cienką warstwą: bierze dane z selectors.py, odpala akcję biznesową z services.py, renderuje szablon.

Kolorowy zbliżenie ekranu z kodem źródłowym w edytorze programisty
Źródło: Pexels | Autor: Muhammed Ensar

Aplikacje w Django – sensowny podział i organizacja kodu

startapp – co powstaje i co z tym zrobić

Tworzenie aplikacji:

python manage.py startapp blog

Django wygeneruje:

  • apps.py – konfiguracja aplikacji, nazwa, etykieta, sygnały,
  • models.py – miejsce na modele,
  • views.py – pierwsze widoki,
  • tests.py – testy jednostkowe,
  • admin.py – rejestracja modeli w panelu admina,
  • migrations/ – migracje bazy danych.

Pierwsza rzecz po startapp: dopisz aplikację do INSTALLED_APPS w settings.py, zwykle w formie referencji do klasy konfiguracyjnej:

INSTALLED_APPS = [
    # ...
    "blog.apps.BlogConfig",
]

Granica między „jedną dużą aplikacją” a „kilkoma mniejszymi”

Na małych projektach kusi, żeby wszystko trzymać w jednej aplikacji: core lub main. Można, ale rodzi to problemy przy rozwoju. Podział aplikacji ma sens, gdy:

  • moduły mają inny cykl życia (np. moduł „faktury” rozwijasz rzadziej niż „zamówienia”),
  • możesz sobie wyobrazić użycie tej aplikacji w innym projekcie,
  • masz osobne zespoły lub osoby odpowiedzialne za określony fragment funkcjonalności.

Praktyczna zasada: jeśli plik models.py w aplikacji ma kilkaset linii i opisuje zupełnie różne byty (np. klienci, faktury, powiadomienia, blog), czas na rozbicie na więcej aplikacji.

apps.py i konfiguracja aplikacji

Plik apps.py zawiera klasę dziedziczącą po AppConfig:

from django.apps import AppConfig

class BlogConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "blog"

Tu możesz dodać inicjalizację logiki specyficznej dla aplikacji, np. rejestrację sygnałów:

class BlogConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "blog"

    def ready(self):
        from . import signals  # noqa

ready() wywoła się po starcie Django. Dzięki temu sygnały nie lądują w models.py i nie robi się z niego „śmietnik”.

urls.py w aplikacjach i include w projekcie

Każda większa aplikacja powinna mieć własny urls.py. Globalny config/urls.py wtedy jedynie dołącza ich namespace:

# config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls", namespace="blog")),
]

W aplikacji:

# blog/urls.py
from django.urls import path
from .views import post_list, post_detail

app_name = "blog"

urlpatterns = [
    path("", post_list, name="post_list"),
    path("<int:pk>/", post_detail, name="post_detail"),
]

Dzięki app_name i namespace możesz odwołać się do adresu w szablonie bez konfliktów:

<a href="{% url 'blog:post_detail' pk=post.pk %}">Szczegóły</a>

Reużywalne aplikacje – co od początku robić „porządnie”

Jeśli aplikacja ma potencjał, by stać się biblioteką (np. „komentarze”, „powiadomienia e-mail”), zadbaj od razu o:

  • brak twardych importów do config i innych aplikacji,
  • czytelny apps.py z nazwą przestrzeni,
  • szablony i statyki w przestrzeni nazw aplikacji (templates/app_name/...),
  • proste API (np. funkcje w services.py), które można wywołać z zewnątrz.

Późniejsze wydzielenie takiego modułu do osobnego repozytorium wtedy nie boli – to tylko kopiowanie katalogu z drobnymi poprawkami.

Konfiguracja settings.py – bezpieczeństwo, modularność, środowiska

Co absolutnie nie powinno trafić do repozytorium

Są elementy konfiguracji, których nie trzymasz „na twardo” w repozytorium:

  • SECRET_KEY,
  • hasła do baz danych,
  • klucze API do zewnętrznych usług (Stripe, AWS, itp.),
  • ustawienia specyficzne dla serwera (np. adresy brokerów, dane SMTP produkcji).

Bezpieczny wzorzec to trzymanie tych danych w zmiennych środowiskowych i wciąganie ich do settings.py przez narzędzie typu python-decouple albo django-environ.

Przykład z użyciem django-environ:

# config/settings/base.py
import environ
import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

env = environ.Env(
    DEBUG=(bool, False),
)

environ.Env.read_env(os.path.join(BASE_DIR, "..", ".env"))

SECRET_KEY = env("SECRET_KEY")
DEBUG = env("DEBUG")
DATABASES = {
    "default": env.db("DATABASE_URL")
}

Pliku .env nie commitujesz, trzymasz go lokalnie lub w managerze sekretów.

Dzielony settings.py – base, dev, prod

Jednolity plik settings.py szybko staje się problemem, gdy trzeba różnie konfigurować produkcję i środowisko deweloperskie. Popularny i prosty układ:

config/
    settings/
        __init__.py
        base.py
        dev.py
        prod.py

W base.py ląduje wszystko, co wspólne:

  • INSTALLED_APPS (bez narzędzi developerskich),
  • MIDDLEWARE,
  • konfiguracja szablonów, i18n, timezone,
  • podstawowe ustawienia statycznych plików.

W dev.py rozszerzasz base o rzeczy deweloperskie:

# config/settings/dev.py
from .base import *

DEBUG = True

INSTALLED_APPS += [
    "debug_toolbar",
]

MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE

W prod.py wymuszasz porządne ustawienia bezpieczeństwa i logowania:

# config/settings/prod.py
from .base import *

DEBUG = False

ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

Zmienną DJANGO_SETTINGS_MODULE ustawiasz np. na config.settings.dev lub config.settings.prod w zależności od środowiska.

Porządkowanie INSTALLED_APPS i MIDDLEWARE

Wraz z rozwojem projektu INSTALLED_APPS potrafi urosnąć do kilkudziesięciu pozycji. Dobrą praktyką jest podział na sekcje:

Grupowanie konfiguracji aplikacji

Przy większym projekcie sam INSTALLED_APPS i MIDDLEWARE w base.py również warto podzielić tematycznie. Zamiast jednej wielkiej listy, trzymaj je w kilku, a na końcu sklejaj:

# config/settings/base.py

DJANGO_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",
]

LOCAL_APPS = [
    "accounts",
    "blog",
    "orders",
]

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

Dzięki temu po pół roku wiesz, gdzie dopisać kolejną aplikację, a gdzie czegoś przypadkiem nie usunąć. Podobnie z middleware:

DJANGO_MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

THIRD_PARTY_MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
]

PROJECT_MIDDLEWARE = [
    "config.middleware.RequestIDMiddleware",
]

MIDDLEWARE = DJANGO_MIDDLEWARE + THIRD_PARTY_MIDDLEWARE + PROJECT_MIDDLEWARE

Jeśli kiedyś trzeba będzie szybko wyciąć z projektu np. CORS lub debug toolbar, nie szukasz po całym pliku – masz to w jednym bloku.

Ustawienia zależne od środowiska – wzorzec „contrib”

Kolejna praktyka to drobny, ale wygodny podział na konfigurację frameworka i „wkład własny” projektu. Przykładowy układ:

config/
    settings/
        contrib/
            django.py
            third_party.py
            logging.py
        base.py
        dev.py
        prod.py

W plikach z katalogu contrib/ trzymasz konfiguracje, które są w większości niezależne od domeny projektu, np. loggerów czy DRF:

# config/settings/contrib/logging.py
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
        },
    },
    "root": {
        "handlers": ["console"],
        "level": "INFO",
    },
}
# config/settings/base.py
from .contrib.logging import LOGGING

Kiedy tworzysz kolejny projekt w firmie, sporo konfiguracji możesz po prostu skopiować z poprzedniego repozytorium. Mniejsza szansa na błąd przy każdym następnym wdrożeniu.

Specyficzne settings dla funkcjonalnych modułów

Przy bardziej rozbudowanych aplikacjach domenowych czasem opłaca się wydzielić oddzielne pliki konfiguracyjne, ale już nie pod kątem środowiska, tylko funkcji. Przykład – moduł płatności:

config/
    settings/
        payments.py
# config/settings/payments.py
from .base import env

PAYMENTS_PROVIDER = env("PAYMENTS_PROVIDER", default="stripe")

STRIPE_PUBLIC_KEY = env("STRIPE_PUBLIC_KEY", default="")
STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY", default="")

Wtedy w kodzie nie doklejasz kolejnych ustawień do rozrastającego się base.py, tylko korzystasz z nich jak z modułu:

# payments/services.py
from django.conf import settings
from config.settings import payments as payments_settings

def charge_customer(customer, amount):
    if payments_settings.PAYMENTS_PROVIDER == "stripe":
        # logika pod Stripe
        ...

Taki układ szczególnie pomaga, gdy w projekcie pojawia się wielu integratorów z zewnętrznymi API. Każdy moduł ma swój wycinek konfiguracji, a plik bazowy nie staje się listą setek stałych.

Konfiguracja per aplikacja – domyślne ustawienia

Nie każdy parametr musi trafić do globalnego settings.py. Czasem wygodniej jest dać rozsądne domyślne wartości w samej aplikacji i pozwolić projektowi nadpisać je tylko wtedy, gdy trzeba.

Przykładowy wzorzec w aplikacji blog:

# blog/conf.py
from django.conf import settings

DEFAULTS = {
    "POSTS_PER_PAGE": 10,
    "ENABLE_COMMENTS": True,
}

class BlogSettings:
    def __getattr__(self, item):
        project_settings = getattr(settings, "BLOG", {})
        if item in project_settings:
            return project_settings[item]
        return DEFAULTS[item]

blog_settings = BlogSettings()

W projekcie możesz wtedy opcjonalnie skonfigurować blog:

# config/settings/base.py
BLOG = {
    "POSTS_PER_PAGE": 5,
}

W kodzie aplikacji operujesz na blog_settings:

# blog/views/public.py
from .conf import blog_settings

def post_list(request):
    per_page = blog_settings.POSTS_PER_PAGE
    ...

Zaleta: reużywalna aplikacja nie wymaga ręcznego dopisywania dziesięciu stałych w settings.py. Projekt, który ich nie poda, dostaje gotowe, sensowne domyślne ustawienia.

Źródła konfiguracji: zmienne środowiskowe, pliki, sekret manager

Dla małego projektu wystarczy plik .env. Przy aplikacjach działających w kilku środowiskach (dev, staging, prod) lepiej z góry przemyśleć, skąd ściągasz sekrety i parametry.

Najczęstszy praktyczny schemat:

  • lokalnie – .env lub .env.local,
  • staging/production – zmienne środowiskowe ustawiane przez system (Docker, Kubernetes, platforma PaaS) lub z managera sekretów.

Przy użyciu Dockera ustawiasz je w docker-compose.yml (lub lepiej: w .env, który czyta docker-compose):

services:
  web:
    image: myproject
    env_file:
      - .env

Na Kubernetesa zwykle lądują w Secret lub ConfigMap. Dzięki temu nie musisz modyfikować samego settings.py przy każdej zmianie środowiska – wystarczy nadpisanie odpowiednich zmiennych.

Bezpieczeństwo w settings – szybka checklista

Kiedy projekt ma zobaczyć produkcję, dobrze przejść przez konkretne punkty. Krótka, praktyczna lista:

  • DEBUG = False – w prod.py i potwierdzone zmienną środowiskową,
  • ALLOWED_HOSTS ustawione na konkretne domeny, nie na *,
  • SECRET_KEY z bezpiecznego źródła, nigdy w repozytorium,
  • SECURE_SSL_REDIRECT = True (chyba że SSL obsługuje reverse proxy i wiesz, co robisz),
  • SESSION_COOKIE_SECURE i CSRF_COOKIE_SECURE na True,
  • CSRF_TRUSTED_ORIGINS zawiera domeny, z których aplikacja ma być wywoływana,
  • logowanie błędów do zewnętrznego systemu (Sentry, ELK, inny stack),
  • wyłączone django-debug-toolbar i inne narzędzia deweloperskie.

W praktyce często wystarcza, że w dev.py masz pełną swobodę, a w prod.py przy każdej zmianie sprawdzasz powyższe pięć–sześć linii. Im wcześniej wprowadzisz taką checklistę, tym mniej „gorących” poprawek po pierwszym deployu.

Konfiguracja statycznych i medialnych plików

Na lokalnym środowisku STATICFILES_DIRS i MEDIA_ROOT działają bezboleśnie, ale przy wdrożeniu na produkcję trzeba mieć spójny schemat na pliki statyczne oraz uploady użytkowników.

Minimalny układ katalogów:

project_root/
    config/
    app1/
    app2/
    static/
    media/

W ustawieniach bazowych:

# config/settings/base.py
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "..", "staticfiles")

MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "..", "media")

Na produkcji:

  • statyczne pliki zwykle serwuje serwer www (Nginx, CDN) po collectstatic,
  • pliki użytkownika lądują w katalogu montowanym jako wolumen lub w zewnętrznym storage (S3, GCS).

Dobrą praktyką jest integracja biblioteki typu django-storages do obsługi zewnętrznego storage’u:

# config/settings/prod.py
from .base import *

DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage"

AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME")

Jeśli przeniesienie storaga zostawisz na sam koniec, migracja plików z lokalnego dysku na S3 będzie bardziej bolesna niż wprowadzenie tego od razu.

Testowe ustawienia – osobny settings dla testów

Przy większej bazie testów unit i integracyjnych sensowne jest lekkie dostosowanie konfiguracji pod ich potrzeby. Można to zrobić w osobnym pliku, np. test.py:

# config/settings/test.py
from .base import *

DEBUG = False

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.MD5PasswordHasher",
]

EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "test-cache",
    }
}

Następnie wskazujesz ten plik w pytest.ini lub komendzie testów:

DJANGO_SETTINGS_MODULE=config.settings.test pytest

Zyskujesz szybsze hashowanie haseł, brak zewnętrznych serwisów (SMTP, Redis) i powtarzalne wyniki testów. Przy TDD lub po prostu dużej liczbie testów różnica w czasie wykonania jest bardzo odczuwalna.

Feature flags i ustawienia przełączników

Przy projektach, które często wypuszczają nowe funkcje, przydają się przełączniki w konfiguracji, pozwalające włączać i wyłączać funkcjonalność bez deployu. Na początek nie trzeba od razu zewnętrznego systemu feature flagów, wystarczy prosty blok w settings.py:

# config/settings/base.py
FEATURE_FLAGS = {
    "NEW_CHECKOUT_FLOW": False,
    "ENABLE_BETA_DASHBOARD": False,
}

W kodzie biznesowym:

from django.conf import settings

def checkout_view(request):
    if settings.FEATURE_FLAGS.get("NEW_CHECKOUT_FLOW"):
        return new_checkout(request)
    return old_checkout(request)

Gdy flaga zaczyna być sterowana z zewnątrz (panel admina, panel operacji), możesz przenieść logikę do bazy danych lub prostego serwisu, a parametr w settings.py zostawić jako domyślny fallback.

Porządek w settings a onboarding nowych osób

Dobrze ułożone settings/ to nie tylko porządek w kodzie. To także szybszy onboarding nowych osób w zespole. Osoba, która dopiero dochodzi do projektu, zwykle zaczyna właśnie od settings.py, żeby zrozumieć używane aplikacje, integracje i podział środowisk.

Jeżeli konfiguracja jest podzielona na czytelne segmenty (base, dev, prod, contrib, moduły funkcjonalne), taka osoba w kilka minut zorientuje się, gdzie dodać nową aplikację, nowe zmienne, nowe integracje. Efekt uboczny – mniej przypadkowych konfliktów i mniej „szybkich hacków” w środku nocy.