Dobre praktyki w Django REST Framework
Django REST Framework (DRF) ułatwia tworzenie API w Django, ale aby zbudować wydajną i bezpieczną aplikację, warto przestrzegać pewnych najlepszych praktyk. Poniżej omawiamy kluczowe aspekty, takie jak optymalizacja widoków i serializerów, zarządzanie uprawnieniami, paginacja i filtrowanie, cache, testowanie, dokumentacja oraz bezpieczeństwo.
1. Optymalizacja widoków i serializerów
Wybór ModelSerializer vs Serializer: W większości przypadków korzystaj z serializers.ModelSerializer
, który automatycznie generuje pola na podstawie modelu Django i dostarcza domyślne implementacje metod create()
i update()
(python - What is the difference between Serializer and ModelSerializer - Stack Overflow) . ModelSerializer to skrót pozwalający szybko zbudować CRUD dla modelu – jest analogiczny do ModelForm w Django. Zwykłego serializers.Serializer
używaj, gdy pola nie odpowiadają bezpośrednio modelowi lub potrzebujesz pełnej kontroli nad serializacją (np. łączenie danych z wielu modeli).
Minimalizowanie liczby zapytań (N+1): Domyślnie DRF nie optymalizuje automatycznie zapytań ORM w serializerach z relacjami (Serializer relations - Django REST framework). Oznacza to, że jeśli serializer odwołuje się do pól z relacji (np. kluczy obcych lub relacji wiele-do-wielu), musimy sami zoptymalizować zapytanie, inaczej pojawi się problem N+1 (dla każdej instancji wykonywane jest dodatkowe zapytanie). Rozwiązanie to użycie odpowiednich metod QuerySet:
select_related
– dla relacji typu ForeignKey/OneToOne (jedna do jednego) – dołączanie powiązanego obiektu w jednym zapytaniu.prefetch_related
– dla relacji wiele-do-jednego i wiele-do-wielu – pobieranie wielu obiektów w jednym dodatkowym zapytaniu.
Dzięki tym metodom możemy znacząco zredukować liczbę zapytań do bazy danych. Przykładowo, pobierając listę obiektów z polem klucza obcego, użycie select_related
pozwoli uniknąć osobnego zapytania dla każdego obiektu (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English). Dla relacji odwrotnych lub M2M prefetch_related
zrealizuje jedno zapytanie dodatkowe zamiast wielu.
Przykład: Załóżmy, że mamy model Album i powiązany tracks
(utwory) jako relację wiele-do-wielu. Serializer AlbumSerializer zwraca listę tytułów utworów. Bez optymalizacji, dla każdego albumu pobranie utworów generuje osobne zapytanie. Rozwiązanie to prefetche w zapytaniu widoku:
# models.py (przykładowo)
class Album(models.Model):
title = models.CharField(max_length=100)
tracks = models.ManyToManyField(Track, related_name="albums")
# serializers.py
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.SlugRelatedField(
many=True, read_only=True, slug_field='title'
)
class Meta:
model = Album
fields = ['id', 'title', 'tracks']
# views.py
from rest_framework.generics import ListAPIView
class AlbumListView(ListAPIView):
queryset = Album.objects.prefetch_related('tracks').all()
serializer_class = AlbumSerializer
Powyżej użyto prefetch_related('tracks')
przed przekazaniem querysetu do serializera. Dzięki temu utwory wszystkich albumów zostaną pobrane z bazy jednym zapytaniem, a serializer nie będzie generował dodatkowych hitów do bazy (Serializer relations - Django REST framework) (Serializer relations - Django REST framework). To samo dotyczy select_related
dla kluczy obcych: np. jeśli Album ma pole artist = ForeignKey(Artist)
, to dodanie .select_related('artist')
pobierze wykonawcę razem z albumem.
Uwaga: Sprawdzaj w Django Debug Toolbar lub logach SQL, czy liczba zapytań jest zoptymalizowana. DRF nie robi tego za Ciebie automatycznie (Serializer relations - Django REST framework), więc każda relacja w serializerze powinna mieć odpowiednio przygotowany queryset.
Unikanie ładowania zbędnych danych: Jeśli serializer wykorzystuje tylko niektóre pola modelu, rozważ użycie QuerySet.only() lub defer() do pobrania tylko potrzebnych kolumn (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English). Np. Article.objects.only('title', 'content')
pobierze tylko te kolumny, co przy dużych modelach może zaoszczędzić czas.
Podsumowanie: Używaj ModelSerializer do szybkiego tworzenia serializerów modelowych, ale zawsze dbaj o optymalizację zapytań w widoku (.get_queryset()
). W przypadku zagnieżdżonych serializerów lub pól relacyjnych, wykorzystaj select_related
/ prefetch_related
, aby uniknąć problemu N+1 i poprawić wydajność API (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English). To szczególnie ważne w dużych projektach, gdzie nawet kilkanaście niepotrzebnych zapytań per żądanie może obciążyć bazę danych.
2. Efektywne użycie permissions i authentication
Zasada domyślna – najmniejsze uprawnienia: Lepiej domyślnie zablokować dostęp do API i otwierać tylko tam, gdzie to wymagane. DRF domyślnie ustawia permissions.AllowAny
(brak ograniczeń) jeśli nie skonfigurujemy inaczej. W konfiguracji można to zmienić:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Powyższe sprawi, że domyślnie każde żądanie wymaga uwierzytelnienia (Permissions - Django REST framework). Niezalogowani użytkownicy dostaną 401 Unauthorized. To dobry punkt wyjścia – następnie dla konkretnych widoków możemy stosować inne klasy uprawnień.
Wbudowane klasy uprawnień:
- AllowAny – brak ograniczeń, każdy ma dostęp (używaj świadomie, np. dla endpointu rejestracji użytkownika).
- IsAuthenticated – wymaga zalogowanego użytkownika (odrzuci anonimowych) (Permissions - Django REST framework).
- IsAdminUser – wymaga, by
request.user.is_staff
było True (tylko administratorzy Django). - IsAuthenticatedOrReadOnly – pozwala wszystkim na bezpieczne metody GET/HEAD/OPTIONS, ale modyfikacja wymaga uwierzytelnienia (Permissions - Django REST framework). Przydatne np. dla publicznego API tylko-do-odczytu, gdzie pisanie mają tylko zalogowani.
- DjangoModelPermissions – mapuje uprawnienia do modeli Django na działania API (Permissions - Django REST framework). Np. wymaga uprawnienia
add_model
żeby zrobić POST,change_model
dla PUT/PATCH,delete_model
dla DELETE. Użytkownik musi być uwierzytelniony i mieć przypisane odpowiednie prawa w Django Admin (Permissions - Django REST framework). Ta klasa zakłada, że widok ma zdefiniowany atrybutqueryset
, by wiedzieć, do jakiego modelu stosować uprawnienia. - DjangoModelPermissionsOrAnonReadOnly – jak wyżej, ale niezalogowani mogą czytać (GET/HEAD/OPTIONS) (Permissions - Django REST framework).
- DjangoObjectPermissions – wymaga zarówno modelowych uprawnień, jak i indywidualnych uprawnień obiektowych (np. z użyciem django-guardian) (Permissions - Django REST framework). Stosowane rzadziej, gdy potrzebna jest kontrola dostępu na poziomie pojedynczych obiektów.
W praktyce, często stosuje się mieszankę IsAuthenticated i DjangoModelPermissions dla typowych aplikacji backoffice. Np.:
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
class InvoiceViewSet(ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
permission_classes = [IsAuthenticated, DjangoModelPermissions]
Taki ViewSet wymaga zalogowania oraz odpowiednich uprawnień do modelu Invoice (dodawanie/edycja/usuwanie) (Permissions - Django REST framework). Jeśli użytkownik jest zalogowany, ale nie ma uprawnienia "change_invoice", otrzyma 403 Forbidden przy próbie modyfikacji.
Własne klasy uprawnień: W DRF możemy tworzyć customowe permisisons dziedzicząc po rest_framework.permissions.BasePermission
. Definiujemy w nich metodę has_permission
(ogólna kontrola na poziomie widoku, np. sprawdzenie roli użytkownika) i/lub has_object_permission
(kontrola dostępu do konkretnego obiektu). Przykład – pozwolenie na edycję obiektu tylko jeśli należy do użytkownika:
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
# read-only (GET, HEAD, OPTIONS) – zawsze True
if request.method in SAFE_METHODS:
return True
# pozostałe – tylko jeśli obiekt należy do użytkownika
return obj.owner == request.user
Taki permission można przypisać do widoku operującego na obiektach, np. PostViewSet, aby tylko właściciel posta mógł go edytować/usunąć, a reszta użytkowników mogła go tylko odczytać. Pamiętaj, że przy uprawnieniach obiektowych warto też ograniczyć queryset, żeby w ogóle nie zwracać użytkownikowi obiektów, do których nie powinien mieć dostępu (Permissions - Django REST framework) (np. nadpisując get_queryset
tak, by filtrować owner=request.user
).
Authentication (uwierzytelnianie): Permissions określają co użytkownik może zrobić, a Authentication zajmuje się kim jest użytkownik (czy jest poprawnie rozpoznany). DRF domyślnie ma kilka klas uwierzytelniania:
- SessionAuthentication – wykorzystuje sesje Django (i wymaga poprawnego CSRF tokenu dla metod modyfikujących, jeśli korzystasz z przeglądarki). Dobre dla interaktywnych aplikacji webowych korzystających z sesji i ciastek.
- BasicAuthentication – proste uwierzytelnianie login/hasło w nagłówku HTTP (nagłówek Authorization: Basic ...). W praktyce rzadko używane w produkcji (chyba tylko do testów lub wewnętrznych API przez SSL), bo przesyła dane w każym żądaniu i nie ma mechanizmu wylogowania bez wygaszenia danych uwierzytelniających po stronie klienta.
- TokenAuthentication – pojedynczy statyczny token (np. model Token powiązany z użytkownikiem). Po zalogowaniu front-end otrzymuje token (np. klucz), który wysyła w każdym żądaniu (Authorization: Token
abc123
). Token można łatwo unieważnić po stronie serwera (usuwając lub rotując go). DRF udostępnia prostą implementację wrest_framework.authtoken
(wymaga dodania aplikacji do INSTALLED_APPS i migracji). To wygodne dla prostych API (np. dla aplikacji mobilnej). - JWT (JSON Web Token) – tokeny samopodpisane zawierające zakodowane informacje o użytkowniku. W DRF dostępne poprzez biblioteki third-party, np. djangorestframework-simplejwt. JWT wysyła się w nagłówku Authorization: Bearer
token
. Zaleta: bezstanowość (serwer nie musi przechowywać tokenów, poza ewentualną czarną listą do unieważniania). Wadą jest konieczność ostrożnego obchodzenia się z tokenem (bezpieczeństwo – o tym w sekcji bezpieczeństwa). JWT jest popularne do skalowalnych API, gdzie nie chcemy trzymać sesji na serwerze.
W ustawieniach DRF możemy określić domyślne klasy uwierzytelniania, np.:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
]
}
Powyższe pozwoli DRF obsłużyć zarówno sesje (przeglądarka, UI do testowania), jak i tokeny. W każdym żądaniu DRF spróbuje uwierzytelnić użytkownika kolejnymi metodami z listy.
Dobra praktyka: Używaj jak najprostszego mechanizmu pasującego do potrzeb. Np. dla publicznego SPA + API często stosuje się JWT (bo API jest na innej domenie niż frontend i nie chce się zarządzać sesjami). Dla aplikacji renderowanej przez Django z DRF używanym tylko do tworzenia API dla komponentów AJAX – wygodne może być SessionAuth (bo użytkownik jest zalogowany klasycznie). Upewnij się, że uprawnienia (permissions) współgrają z uwierzytelnianiem – np. IsAuthenticated
zakłada, że któraś metoda uwierzytelnienia zidentyfikowała użytkownika (Session, Token, JWT itp.).
Podsumowując: restrykcyjnie kontroluj dostęp do endpointów. Domyślnie wymagaj logowania (IsAuthenticated) i tylko tam, gdzie celowo chcesz dopuścić anonimowych, użyj AllowAny lub OrReadOnly. Wykorzystuj DjangoModelPermissions, jeśli masz zdefiniowaną politykę uprawnień w modelach (to automatycznie integruje się z adminem Django) (Permissions - Django REST framework). Twórz customowe permission classes dla bardziej złożonych reguł biznesowych (np. limitowanie akcji do właściciela obiektu). I wreszcie – zapewnij poprawne uwierzytelnienie użytkowników, najlepiej przez bezpieczne metody (tokeny lub JWT zamiast BasicAuth, chyba że to konieczne).

3. Strategie paginacji i filtrowania danych
Paginacja (stronicowanie wyników): Dla endpointów listujących wiele obiektów należy włączyć paginację – zapobiega to zwracaniu tysięcy rekordów w jednym żądaniu, co obciąża serwer i sieć. DRF oferuje kilka stylów paginacji:
CursorPagination – paginacja oparta o kursory (tokeny) zamiast bezpośrednich numerów/offsetów. Wykorzystuje unikalny znacznik (np. zakodowany klucz/czas), aby zapewnić stabilną kolejność nawet gdy dane się zmieniają. Zapytania wyglądają jak /api/items/?cursor=cD0yMDIxLTA...
(cursor jest generowany przez serwer). Jest to bardziej zaawansowane, ale wydajne przy dużych zbiorach danych, zwłaszcza gdy sortujemy po czymś unikalnym (np. data utworzenia) (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English). CursorPagination unika problemu "przeskakujących" elementów między stronami gdy dane są dodawane/usuwane. Konfiguracja globalna:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
'PAGE_SIZE': 20,
}
(Trzeba też ustawić ordering
w klasie CursorPagination albo w widoku, aby wiedziała po czym generować kursory).
LimitOffsetPagination – paginacja oparta na parametrach limit
i offset
. Klient określa ile elementów chce i od którego offsetu, np. /api/items/?limit=50&offset=100
zwróci 50 wyników począwszy od setnego. Ten styl bywa używany w bardziej elastycznych API, gdy klient chce sam decydować ile pobiera na stronę (zwykle i tak ustawiamy jakiś maksymalny limit by nie przeciążyć serwera). Można go ustawić globalnie lub per-widok. Globalnie:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 50, # domyślny limit jeśli klient nie poda ?limit=
}
Taki zapis ustawia limit/offset jako domyślną paginację i np. ograniczy domyślnie do 50 wyników (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English). Klient może podać mniejszy limit, a jeśli poda większy, można w klasie paginacji zdefiniować max_limit
, żeby wymusić maksymalny rozmiar.
PageNumberPagination – najprostsza, oparta o numer strony. Klient pyta np. /api/items/?page=2
aby dostać drugą stronę wyników. W DRF domyślnie jest ustawiony PageNumberPagination
z rozmiarem strony (PAGE_SIZE) = None (co oznacza brak limitu). Można globalnie ustawić domyślny rozmiar strony:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20
}
Przykład powyżej ustawia stronę o 20 elementach. W rezultacie np. zapytanie GET /items/ zwróci domyślnie 20 pierwszych elementów, a w odpowiedzi pola next
i previous
wskażą URL kolejnej/poprzedniej strony.
Wybór paginacji zależy od wymagań API. Najczęściej używa się PageNumber (intuicyjna) lub LimitOffset (elastyczna, styl jak w wielu bazach danych czy API np. Twittera). Cursor stosuje się przy naprawdę dużych danych lub potrzebie bezpiecznej paginacji co do konsystencji wyników.
Filtrowanie danych: Umożliwienie klientom filtrowania wyników API poprzez parametry zapytań (query params) czyni API bardziej użytecznym i zapobiega nadmiernemu przesyłaniu danych. DRF nie ma wbudowanego zaawansowanego mechanizmu filtrowania, ale świetnie integruje się z pakietem django-filter.
Kroki by dodać filtrowanie:
- Zainstaluj
django-filter
i dodaj doINSTALLED_APPS
. - Oprócz filtrowania dokładnego, DRF ma też SearchFilter i OrderingFilter:
- OrderingFilter pozwala sortować wyniki poprzez parametr
?ordering=
. Domyślnie pozwala sortować po wszystkich polach modelu, ale można ograniczyć przezordering_fields
. Np.ordering_fields = ['name', 'created']
pozwoli tylko po nazwie lub dacie tworzenia. Przykład użycia już w powyższym fragmencie kodu (dodaliśmy OrderingFilter obok SearchFilter). Klient może wywołać/api/customers/?ordering=name
lub?ordering=-created
(minus oznacza malejąco).
- OrderingFilter pozwala sortować wyniki poprzez parametr
SearchFilter pozwala wyszukiwać po określonych polach (często tekstowych) przy użyciu parametru ?search=...
. Wystarczy dodać:
from rest_framework.filters import SearchFilter, OrderingFilter
class CustomerList(ListAPIView):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['name', 'email']
Wówczas zapytanie /api/customers/?search=Jan
zwróci klientów, których imię lub email zawiera "Jan". (Pod spodem to robi __icontains
na tych polach).
Na poziomie widoku określ pola do filtrowania. Dla widoków opartych o GenericAPIView (ListAPIView, ViewSet itp.) można ustawić atrybut filterset_fields
lub bardziej zaawansowanie filterset_class
. Prostsze podejście:
from django_filters.rest_framework import DjangoFilterBackend
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['category', 'in_stock']
Powyższe pozwoli filtrować /api/products/?category=electronics&in_stock=True
automatycznie po tych polach (Integration with DRF - django-filter 25.1 documentation). Django-filter sam zbuduje FilterSet oparty o model Product
i pola podane na liście (tu: category
i in_stock
). Jeśli potrzebne są filtry z operatorem (gt, lt, icontains itp.), można jawnie zdefiniować FilterSet:
import django_filters.rest_framework as filters
class ProductFilter(filters.FilterSet):
min_price = filters.NumberFilter(field_name="price", lookup_expr='gte')
max_price = filters.NumberFilter(field_name="price", lookup_expr='lte')
class Meta:
model = Product
fields = ['category', 'in_stock', 'min_price', 'max_price']
I we widoku:
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_class = ProductFilter
Teraz API obsłuży zapytania typu /api/products/?min_price=10&max_price=100&category=Books
.
W ustawieniach DRF dodaj domyślny backend filtrujący:
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}
Dzięki temu wszystkie widoki generics będą obsługiwać filtrowanie, jeśli je skonfigurujemy (Integration with DRF - django-filter 25.1 documentation).
Najlepsze praktyki filtrowania: Używaj django-filter do obsługi wielu scenariuszy filtrowania bez pisania tego "ręcznie". Kod będzie krótszy i mniej podatny na błędy, a dodatkowo dostajemy walidację typów (np. jeśli oczekujemy liczby, django-filter samo zwróci błąd 400 gdy parametru nie da się zrzutować). Pamiętaj, by dla często filtrowanych pól mieć założone indeksy w bazie danych – inaczej duże zapytania mogą być powolne. Django przy polach typu ForeignKey/CharField automatycznie dodaje indeksy (np. klucze obce, unikalne pola), ale np. dla Boolean czy Date założenie indeksu pod filtr warto rozważyć jeśli to krytyczne.
Unikaj budowania skomplikowanych filtrów za pomocą własnych parametrów bez django-filter, bo łatwo zrobić literówkę lub pominąć walidację. Jeśli jednak logika jest bardzo niestandardowa, możesz w metodzie get_queryset
sprawdzić request.query_params
i odpowiednio zmodyfikować queryset. Przykład:
def get_queryset(self):
qs = Invoice.objects.all()
status = self.request.query_params.get('status')
if status:
qs = qs.filter(status=status)
return qs
To szybkie dla prostego przypadku, ale dla większej liczby parametrów robi się uciążliwe – wtedy definicja FilterSet jest lepsza.
Paginuąc i filtrując jednocześnie: DRF automatycznie stosuje paginację po filtracji – tzn. najpierw queryset jest filtrowany, potem ograniczany do wybranej strony. Dzięki temu np. możesz pobrać 2 stronę wyników filtra ?category=Books
. Warto też obsługiwać przypadek braku wyników czy skrajnych stron (DRF zwróci wtedy puste wyniki na stronie i odpowiednie linki next/prev).
4. Korzystanie z cache i optymalizacji zapytań ORM
Wykorzystanie cache dla często odczytywanych danych: Cache (pamięć podręczna) pozwala przyspieszyć API, zwłaszcza jeśli pewne zasoby są często pobierane i rzadko się zmieniają. Django ma wbudowany framework cache (obsługuje różne backendy: lokalny memory, memcached, Redis itp.). W kontekście DRF są dwa główne podejścia do cachowania:
Cache na poziomie funkcji/fragmentu: Czasem nie chcemy cache'ować całej odpowiedzi, tylko wynik jakiegoś kosztownego obliczenia lub zapytania. Możemy użyć django.core.cache.cache
bezpośrednio. Przykład – mamy funkcję generującą duży raport lub pobierającą dane z zewnętrznego API:
from django.core.cache import cache
def get_exchange_rates():
rates = cache.get('exchange_rates')
if rates is None:
# zakładamy, że fetch_rates() pobiera dane z zewnętrznego API
rates = fetch_rates()
cache.set('exchange_rates', rates, 60*60) # cache na godzinę
return rates
Powyżej przy pierwszym wywołaniu dane zostaną pobrane i zapisane, a kolejne wywołania w ciągu godziny zwrócą dane z pamięci podręcznej zamiast wywoływać ponownie kosztowną funkcję (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English).
Cache całych widoków (wyniku odpowiedzi) – jeśli np. mamy endpoint listy, który rzadko się zmienia, możemy cache'ować jego odpowiedź JSON na określony czas. Najprostsze jest użycie dekoratora cache_page
z Django. Dla widoków funkcyjnych dekorujemy bezpośrednio, dla klas DRF używamy method_decorator
. Przykładowo:
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
class ArticleListView(ListAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@method_decorator(cache_page(60*15)) # cache na 15 minut
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
Powyżej metoda list
(obsługująca GET na ListAPIView) jest opakowana dekoratorem cache, więc pierwsze wywołanie zapisze wynik w cache na 15 minut, a kolejne żądania w tym czasie zwrócą zawartość z cache (bez wykonywania ponownie logiki widoku ani zapytań do bazy) (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English) (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English).Inną opcją jest udekorowanie metody dispatch
– wtedy cały widok (wszystkie metody) będzie cache'owany, ale zwykle chcemy cache'ować tylko GET (bo cache PUT/POST raczej nie ma sensu). W DRF 3.12+ pojawił się też dekorator @extend_schema
do dokumentacji – on nie wpływa na cache, to inna rzecz.W przypadku danych zależnych od użytkownika (autoryzacja), cache trzeba łączyć z dodatkowymi nagłówkami. Np. @vary_on_cookie
lub @vary_on_headers("Authorization")
powoduje rozróżnienie cache w zależności od cookie sesyjnego lub tokenu auth (Caching - Django REST framework) (Caching - Django REST framework). W ten sposób możemy cache'ować stronę dla każdego użytkownika osobno. Przykładowo:
class ProfileView(APIView):
@method_decorator(cache_page(60*60))
@method_decorator(vary_on_headers("Authorization"))
def get(self, request):
data = heavy_db_calculation_for_user(request.user)
return Response(data)
spowoduje, że każdy unikalny token (użytkownik) ma swoją kopię cacha na godzinę (Caching - Django REST framework).
W kontekście DRF, możemy podobnie cache'ować kosztowne fragmenty w metodach widoku. Np. w get
:
def get(self, request):
key = f"user_stats_{request.user.id}"
data = cache.get(key)
if not data:
data = compute_user_stats(request.user)
cache.set(key, data, 300)
return Response(data)
To cache'uje wynik obliczeń statystyk użytkownika na 5 minut pod kluczem unikalnym dla usera.
Backend cache: W środowisku produkcyjnym warto użyć wydajnego backendu jak Redis. Konfiguracja w Django może wyglądać np.:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
Taki config (z użyciem biblioteki django-redis
) ustawia cache domyślny na Redis działający lokalnie ([
How to Cache Django REST Framework with Redis
](https://tute.io/how-to-cache-django-rest-framework-with-redis#:~:text=CACHES%20%3D%20%7B%20,%7D%20%7D)). Wykorzystanie Redis zapewnia bardzo szybkie odczyty/zapisy cache, nawet przy wielu procesach aplikacji.
Inwalidacja cache: Pamiętaj, że cache niesie ryzyko zwracania nieaktualnych danych. Strategia zależy od wymagań:
- Ustawiaj względnie krótki czas życia (TTL) – np. kilka minut – jeśli dane mogą się często zmieniać, żeby użytkownik nie widział długo starych danych.
- Możesz też aktywnie czyścić cache w sygnałach Django. Np. po zapisaniu modelu, wywołać
cache.delete('nazwa_klucza')
, aby usunąć nieaktualną wartość. - Przy
cache_page
, można użyć wersjonowania URL lub dołączania do klucza jakiegoś parametru, aby łatwo unieważnić (np. zmieniając URL schemę). W Django cache_key dla cache_page opiera się o pełny URL, więc zmiana nawet dummy parametru w linku może wymusić odświeżenie.
Optymalizacja ORM: Oprócz użycia cache, pamiętajmy o profilaktycznej optymalizacji zapytań:
- Wykorzystaj wskazane wcześniej
select_related
iprefetch_related
– cache nie zwalnia z pisania wydajnego kodu! Cache jest pomocny, gdy wiele razy odczytujemy te same dane. Ale np. dla jednorazowych raportów złożoność zapytań nadal ma znaczenie. - Unikaj wykonywania zapytań w pętli. Typowy błąd: iteracja po queryset w Pythonie i wewnątrz odwoływanie się do relacji – to znów N+1 problem. Lepiej od razu zapytać z joinem/prefetch.
- Jeżeli musisz użyć SerializerMethodField lub własnych właściwości, które odwołują się do bazy, rozważ przeniesienie logiki do adnotacji querysetu. Np. chcesz zwrócić liczbę elementów w relacji – zamiast w SerializerMethodField robić
return obj.related_set.count()
(które wykona zapytanie COUNT dla każdego obiektu), możesz w widoku zrobić:queryset = MyModel.objects.annotate(rel_count=Count('related'))
i potem w serializerze mieć polerel_count
. Dzięki temu liczności zostaną obliczone jednym zapytaniem z GROUP BY, zamiast wielu pojedynczych. - Jeśli często pobierasz duże querysety, rozważ paginację także po stronie bazy (np. iterowanie po pk, użycie QuerySet.iterator()) lub w ogóle streaming wyników – to już przy ekstremalnych przypadkach.
Podsumowanie: Stosuj cache tam, gdzie ma to sens – np. publiczne, rzadko zmieniające się dane (listy artykułów, rankingi itp.) mogą być keszowane minutami lub godzinami, co zdejmuje obciążenie z bazy danych. W pamięci podręcznej trzymaj też wyniki kosztownych operacji, by nie przeliczać ich za każdym razem. Jednak nie opieraj wydajności tylko na cache – zawsze dąż do zminimalizowania pracy bazy danych poprzez przemyślane zapytania (joiny, prefetch, agregacje). Kombinacja poprawnie napisanego ORM + cache daje najlepsze rezultaty w dużych projektach. Dzięki temu unikniesz zarówno powolnych zapytań, jak i przeciążenia aplikacji pod wysokim ruchem.
5. Testowanie API
Dlaczego testować API? API często jest kontraktem dla frontendu lub klientów zewnętrznych. Błędy w API mogą być trudne do wykrycia ręcznie, dlatego warto pisać automatyczne testy, które sprawdzą czy odpowiedzi mają właściwy format, uprawnienia działają, walidacje zwracają błędy itp. Testy dają pewność przy refaktoryzacjach czy aktualizacjach biblioteki, że nie zepsuliśmy istniejącego zachowania.
DRF ułatwia testowanie dostarczając narzędzia w rest_framework.test
. Najważniejsze to:
- APIClient – obiekt działający podobnie do Django
Client
, ale obsługujący wygodnie API (np. automatycznie serializuje dane do JSON jeśli podamyformat='json'
). Można go używać zarówno w klasach testowych opartych o unittest, jak i w pytest. - APITestCase – klasa testowa dziedzicząca po Django TestCase, która ma wbudowany
self.client
będący APIClientem (Testing - Django REST framework). Dzięki temu nie musimy tworzyć klienta, możemy od razu wywoływaćself.client.get(...)
,self.client.post(...)
w testach. APITestCase ustawia też domyślny format żądań na JSON (można zmienić przez ustawienie, ale generalnie upraszcza to pisanie testów API).
Przykład testu z APITestCase:
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Account
class AccountTests(APITestCase):
def test_create_account(self):
url = reverse('account-list') # używamy nazwanej trasy z routera ViewSet
data = {'name': 'DabApps'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Account.objects.count(), 1)
self.assertEqual(Account.objects.get().name, 'DabApps')
Powyższy test sprawdza, czy możemy utworzyć nowy obiekt Account przez API (Testing - Django REST framework). Używamy reverse
żeby uzyskać URL (to lepsze niż hardcode'owanie ścieżki), wysyłamy POST z danymi JSON i weryfikujemy:
- Kod odpowiedzi to 201 (Created).
- Liczba obiektów w bazie wzrosła o 1.
- Właściwości obiektu w bazie są zgodne z tym, co wysłaliśmy.
Testowanie różnych scenariuszy: W praktyce należy przetestować:
- Scenariusze sukcesu – poprawne żądania zwracają oczekiwane dane (np. tworzenie obiektu, pobieranie listy, aktualizacja).
- Scenariusze błędów – np. walidacja danych (API powinno zwrócić 400 z odpowiednim komunikatem, gdy brakuje wymaganego pola), brak uprawnień (401 Unauthorized lub 403 Forbidden gdy użytkownik nie ma dostępu), odwołanie do nieistniejącego zasobu (404).
- Uprawnienia – np. że niezalogowany użytkownik nie może utworzyć obiektu (powinno dać 401), albo że użytkownik bez uprawnień edycji dostaje 403. Można tworzyć różnych użytkowników w testach (
User.objects.create_user
) i używaćself.client.force_authenticate(user=...)
(jeśli APITestCase, toself.client.login(...)
dla SessionAuth, lub ustawić token w nagłówku). - Różne kombinacje parametrów – np. paginacja (sprawdź że drugi page zwraca oczekiwane elementy), filtrowanie (że ?search czy ?filter zwraca prawidłowe wyniki).
Testowanie w pytest: Alternatywnie do klas APITestCase, wiele projektów używa pytest
z wtyczką pytest-django
. Wtedy można pisać testy funkcjonalne. APIClient jest kompatybilny z pytest – można go użyć jako fixture. Przykład testu w pytest:
import pytest
from rest_framework.test import APIClient
from django.urls import reverse
@pytest.mark.django_db
def test_create_book():
client = APIClient()
url = reverse('book-list')
data = {"title": "Nowa Książka", "author": 1}
response = client.post(url, data, format='json')
assert response.status_code == 201
assert response.data["title"] == "Nowa Książka"
Pytest ma czytelniejszą składnię asercji i potrafi lepiej raportować błędy, stąd rosnąca popularność. W pytest
możemy też parametryzować testy (sprawdzić wiele wariantów danych wejściowych w jednym teście) czy korzystać z fixture do przygotowania obiektów (np. używając factory-boy do tworzenia instancji modeli).
Mockowanie zewnętrznych usług: Jeśli nasze widoki integrują się z zewnętrznymi API (np. podczas żądania DRF wywołujemy jakieś requests.get
do innego serwisu), w testach nie chcemy robić realnych połączeń sieciowych. Można użyć unittest.mock do zamockowania takich wywołań. Przykład użycia dekoratora @patch
na funkcji zewnętrznej biblioteki:
from unittest.mock import patch
@patch('requests.get')
def test_fetch_data_from_api(mock_get):
# Konfigurujemy wartość zwracaną przez requests.get
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"result": "ok"}
# Teraz wywołujemy nasz kod, który internie robi requests.get
response = self.client.get('/api/external-data/', format='json')
# Sprawdzamy, czy odpowiedź API wykorzystuje nasze zamockowane dane
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["result"], "ok")
# Sprawdzamy, że nasz kod woła external API z odpowiednim URL
mock_get.assert_called_once_with("https://external.service/api/data")
W powyższym przykładzie przy pomocy patch
podmieniamy funkcję requests.get
na mock. Ustawiamy, że każde wywołanie requests.get
zwróci obiekt z status_code=200
i metodą .json()
zwracającą słownik {"result": "ok"}. Następnie wykonujemy żądanie do naszego API (które w implementacji robi requests do zewnętrznego serwisu). Dzięki mockowi nie wykonujemy prawdziwego połączenia, a nasz kod dostaje od razu przygotowaną odpowiedź. Na końcu asercje potwierdzają, że odpowiedź API jest prawidłowa oraz że zewnętrzny serwis został wywołany z oczekiwanym URL (Testing APIs with PyTest: How to Effectively Use Mocks in Python) (Testing APIs with PyTest: How to Effectively Use Mocks in Python).
Alternatywnie można użyć biblioteki responses lub requests-mock do imitowania odpowiedzi HTTP zewnętrznych serwerów w kontekście testów.
Testowanie warstwy serializacji/walidacji: Często dobrze jest też przetestować same serializery – czy walidują dane jak zakładamy. Można bezpośrednio tworzyć serializer z danymi i wywoływać is_valid()
, sprawdzając serializer.errors
czy serializer.validated_data
. Np.:
def test_serializer_validation():
serializer = UserSerializer(data={"username": "short", "password": "123"})
assert not serializer.is_valid()
assert "password" in serializer.errors
Takie testy szybciej wskażą problem w logice walidacji niż debugowanie przez widok.
Coverage i ciągła integracja: Staraj się objąć testami jak najwięcej krytycznych ścieżek kodu. W dużych projektach integruje się testy w pipeline CI/CD – każda zmiana kodu uruchamia testy. Warto również mierzyć pokrycie kodu (coverage) – nie tyle dla samej liczby, co żeby zobaczyć które moduły nie są testowane i świadomie zdecydować czy brak testów tam jest OK czy jednak coś dodamy.
Podsumowując, DRF plus Django daje bogate możliwości testowania, od jednostkowych po integracyjne. Wykorzystuj APITestCase/APIClient do symulacji rzeczywistych żądań HTTP do swoich widoków. Mockuj zależności zewnętrzne, aby testy były hermetyczne i szybkie. Testy API powinny sprawdzać zarówno poprawne działanie (ścieżki sukcesu), jak i odpowiednie komunikaty błędów i kody statusu w sytuacjach błędowych. Dobrze przetestowane API to mniej bugów na produkcji i większa pewność przy modyfikacjach w przyszłości.
6. Integracja z OpenAPI/Swagger
Dobra dokumentacja API jest kluczowa w dużych projektach. Dzięki niej inni deweloperzy (albo my za kilka miesięcy) mogą szybko zrozumieć jak korzystać z endpointów. Ręczne pisanie dokumentacji bywa żmudne i podatne na rozjechanie się z kodem, ale DRF oferuje mechanizmy automatycznego generowania specyfikacji OpenAPI (Swagger) na podstawie kodu.
Wbudowany generator schematów: DRF posiada własny generator OpenAPI (dawniej oparte o coreapi i OpenAPI 3 w nowszych wersjach). Jednak obecnie jest on oznaczony jako przestarzały (deprecated) i zaleca się użycie zewnętrznych pakietów (Documenting your API - Django REST framework). Najpopularniejsze to:
- drf-yasg (Yet Another Swagger Generator) – generator dokumentacji zgodnej z OpenAPI 2.0 (Swagger 2) z interfejsem web (Swagger UI i ReDoc).
- drf-spectacular – nowsze narzędzie generujące specyfikację OpenAPI 3.0 i również udostępniające Swagger UI/Redoc. Jest aktywnie rozwijane i rekomendowane przez twórców DRF (Documenting your API - Django REST framework).
Oba narzędzia skanują nasze viewsety, serializery, modele i na tej podstawie tworzą schemat API. Wiele rzeczy zgaduje automatycznie (np. pola wymagane na podstawie blank=False
, typy danych na podstawie typów pól Django, itp.).
drf-yasg – konfiguracja podstawowa:
- Zainstaluj pakiet (
pip install drf-yasg
). - Dodaj
'drf_yasg'
doINSTALLED_APPS
(wymaga też mieć'django.contrib.staticfiles'
bo będzie serwować pliki swagger UI) (drf-yasg - Yet another Swagger generator — drf-yasg 1.21.7 documentation). - W pliku URLS skonfiguruj widok schema:
from django.urls import path, re_path
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="Moje API",
default_version='v1',
description="Opis API",
contact=openapi.Contact(email="[email protected]"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path('swagger<str:format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
# ... inne URLConf
]
Powyższy kod tworzy schema_view
z meta informacjami o API (tytuł, wersja, kontakt, licencja itp.) i dodaje trzy trasy:
/swagger.json
(lub `.yaml) – czysta specyfikacja OpenAPI w formacie JSON/YAML (drf-yasg - Yet another Swagger generator — drf-yasg 1.21.7 documentation),/swagger/
– interfejs Swagger UI (ładna, interaktywna dokumentacja) (drf-yasg - Yet another Swagger generator — drf-yasg 1.21.7 documentation),/redoc/
– alternatywny interfejs dokumentacji (Google ReDoc) (drf-yasg - Yet another Swagger generator — drf-yasg 1.21.7 documentation).
Teraz, uruchamiając serwer i wchodząc na /swagger/
, powinniśmy zobaczyć stronę Swagger UI z listą naszych endpointów, modelami danych itd. Wszystko to generowane automatycznie. drf-yasg stara się odwzorować jak najwięcej szczegółów – łącznie z różnymi kodami odpowiedzi, zagnieżdżonymi serializerami, walidatorami unikalności itp.
Porada: Można kontrolować pewne rzeczy dekoratorami, np. jeśli jakiś widok nie powinien być uwzględniony w schemacie, można dać@swagger_auto_schema(auto_schema=None)
lub aby dodać opisy parametrów, ciała żądania itd., używa się dekoratora@swagger_auto_schema
z biblioteki drf_yasg. Dokumentacja drf-yasg opisuje te możliwości.
drf-spectacular – konfiguracja podstawowa:
- Instalacja:
pip install drf-spectacular
i dodanie do INSTALLED_APPS ('drf_spectacular'
).
W URLConf dodaj endpointy:
from django.urls import path
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
# ... inne ścieżki
]
Ustaw w settings domyślny generator schematu DRF, aby używał spectacular:
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
(Od DRF 3.13+ w sumie i tak preferowane jest użycie zewnętrznego pakietu, ale to zapewnia integrację).
To bardzo podobne do drf-yasg. Różnica: najpierw generujemy surowy schemat pod /api/schema/
(np. klikając to przeglądarka pobierze plik YAML), a Swagger UI dostępny pod /api/docs/
będzie korzystał z tamtego schematu (drf-spectacular - drf-spectacular documentation). Redoc analogicznie.
drf-spectacular wymaga minimalnej konfiguracji i również pozwala dodawać dekorator @extend_schema
aby nadpisać lub uzupełnić informacje w dokumentacji jeśli domyślne generowanie to za mało (np. dodać opis dodatkowego parametru zapytania, przykład odpowiedzi itp.) (drf-spectacular - drf-spectacular documentation) (drf-spectacular - drf-spectacular documentation).
Zaletą drf-spectacular jest pełne wsparcie OpenAPI3 (czyli nowszego standardu) oraz aktywne wsparcie. drf-yasg obsługuje tylko OpenAPI2 (Swagger 2.0) i od jakiegoś czasu nie jest intensywnie rozwijany, ale wciąż jest używany w wielu projektach (jego plusem jest dojrzałość i bogata obsługa wielu edge-case'ów w DRF). Jeśli zaczynasz nowy projekt, rozważ drf-spectacular jako nowocześniejszy wybór (Documenting your API - Django REST framework), tym bardziej że twórcy DRF go rekomendują.
Generowana dokumentacja: Wygenerowana strona Swagger UI pozwala testować API – można na niej wykonywać żądania, jeśli skonfigurujemy mechanizm autoryzacji (Swagger UI obsługuje np. wpisanie tokena JWT czy tokena Basic). To bardzo pomocne przy front-endzie lub dla zewnętrznych developerów korzystających z Twojego API.
Aktualizacja dokumentacji: Najlepsze jest to, że dokumentacja aktualizuje się wraz z kodem. Dodając nowy endpoint w ViewSet, po restartcie aplikacji automatycznie pojawi się on w specyfikacji. Oczywiście opisy tekstowe trzeba dodać samemu (np. docstringi metod będą traktowane jako opisy endpointów), ale i to można automatyzować – np. opis modelu może służyć jako opis w schema.
Przykład integracji: Załóżmy, że dodajemy nowy endpoint /api/login/
zwracający JWT. Jeśli nie zaktualizujemy dokumentacji, użytkownicy API mogą nie wiedzieć jak go użyć. Z drf-spectacular możemy zrobić:
class LoginView(APIView):
@extend_schema(request=LoginSerializer, responses={200: AuthTokenSerializer})
def post(self, request):
...
I odpowiednie serializery, co sprawi, że w dokumentacji pojawi się ten endpoint z opisem oczekiwanego body (LoginSerializer) i odpowiedzi (AuthTokenSerializer). Dla drf-yasg byłoby podobnie z @swagger_auto_schema
.
Najlepsze praktyki dokumentowania API:
- Udostępnij interaktywną dokumentację (Swagger UI/Redoc) zwłaszcza jeśli API będzie używane przez zewnętrznych developerów. To skraca czas integracji i zmniejsza liczbę pytań.
- Jeżeli API jest wewnętrzne, i tak warto mieć schemat OpenAPI – choćby po to, żeby generować klientów (są narzędzia do generowania kodu klienckiego w różnych językach z definicji OpenAPI).
- Pamiętaj o bezpieczeństwie dokumentacji – czasem nie chcesz, by np. niezalogowany użytkownik widział wszystkie endpointy. Możesz zabezpieczyć widok dokumentacji za pomocą np.
IsAuthenticated
(dodającpermission_classes
wget_schema_view
lub używając swojego widoku z SpectacularAPIView). Alternatywnie hostuj dokumentację na osobnej stronie niepublicznej. - Staraj się uzupełniać ręcznie te elementy, których generator nie wie – np. opis pola enum, dodatkowe informacje o zależnościach parametrów, przykładowe wartości. Dobrze udokumentowane API zawiera nie tylko listing endpointów, ale też dla każdego pola opis co oznacza. Część z tego możemy wstawić poprzez argument
help_text
w modelach lub serializerach – drf-spectacular i yasg często je przechwytują do dokumentacji. - Aktualizuj dokumentację przy zmianach w API – użycie generatora minimalizuje wysiłek, ale np. zapominając dodać help_text lub uaktualnić opis w docstringu, możemy wprowadzić dezinformację. Ustanów w zespole praktykę, że każda zmiana endpointu powinna obejmować sprawdzenie dokumentacji (choćby czy schemat generuje to, co chcemy).
Dzięki integracji z OpenAPI, nasze API jest samoopisujące się – co jest celem architektur REST. W dużych projektach to wręcz konieczność, bo liczba endpointów i modeli może być duża. Narzędzia jak drf-yasg czy drf-spectacular stają się wtedy nieocenione, utrzymując dokumentację w ryzach automatycznie.
7. Bezpieczeństwo w DRF
Tworzenie bezpiecznego API to zbiór wielu dobrych praktyk. Wiele z nich dotyczy ogólnie Django, ale są też specyficzne dla REST API. Poniżej kilka obszarów, na które trzeba zwrócić uwagę, by uniknąć typowych błędów:
CSRF (Cross-Site Request Forgery):
Ataki CSRF polegają na tym, że złośliwa strona próbuje wymusić na przeglądarce zalogowanego użytkownika wykonanie akcji (np. POST do naszego API) bez jego wiedzy. W klasycznych aplikacjach Django chronimy się poprzez token CSRF w formularzach. W DRF sytuacja zależy od używanej autentykacji:
- SessionAuthentication w DRF wymaga ochrony CSRF tak samo jak zwykłe widoki Django. Oznacza to, że jeśli front-end korzysta z ciastek sesyjnych, każdy POST/PUT/DELETE musi zawierać prawidłowy token CSRF (np. w nagłówku
X-CSRFToken
). Jeśli go zabraknie, DRF zwróci 403 Forbidden. Dlatego przy budowie SPA opartego o sesje (rzadziej spotykane, ale możliwe) pamiętaj o mechanizmie przekazywania tokenu CSRF z szablonu do JS (Django generuje csrftoken cookie automatycznie, trzeba go odczytać i wysłać). - TokenAuthentication / JWT – tutaj co do zasady nie używa się CSRF, bo żądania są uwierzytelniane nagłówkiem, nie ciasteczkiem. Jeśli nasz front-end nie używa cookies, to nie grozi mu typowy CSRF (atak CSRF wymaga, by uwierzytelnienie było automatyczne poprzez cookie przeglądarki). Natomiast uwaga: czasem implementuje się JWT trzymane w cookie HttpOnly (dla bezpieczeństwa przed XSS). Wtedy mimo użycia JWT, mamy go w ciasteczku i przeglądarka wyśle go automatycznie – czyli atak CSRF znów staje się możliwy. W takiej sytuacji należy również stosować token CSRF! Rozwiązaniem jest np. przechowywanie tokenu odświeżającego w HttpOnly cookie i użycie dodatkowego mechanizmu Double Submit Cookie lub po prostu również trzymanie tokenu CSRF i wymaganie go.
- Jeśli budujesz czysto publiczne API bez cookie (np. tylko token w nagłówku), możesz całkowicie wyłączyć mechanizm CSRF na poziomie DRF (np. wyrzucając SessionAuthentication z DEFAULT_AUTHENTICATION_CLASSES). Ale upewnij się, że na żadnej trasie nie polegasz na cookie session.
Podsumowując: nie wyłączaj pochopnie sprawdzania CSRF – zrób to tylko świadomie, gdy wiesz, że nie będzie potrzebne. A jeśli korzystasz z cookie (sesyjnych lub JWT w cookie), traktuj CSRF jako obowiązkowy element bezpieczeństwa (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI).
Ograniczenie liczby żądań (Rate Limiting / Throttling):
API wystawione publicznie powinno chronić się przed nadmiernym obciążeniem i atakami brute-force. DRF udostępnia mechanizm Throttling – bardzo łatwy w użyciu. Można go włączyć globalnie w ustawieniach:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day',
}
}
Przykładowo powyżej ograniczamy niezalogowanych do 100 żądań dziennie, a zalogowanych użytkowników do 1000 żądań dziennie (How to throttle your API with Django Rest Framework - DEV Community). Gdy limit zostanie przekroczony, DRF automatycznie zwróci błąd 429 Too Many Requests. Możemy też ustawić różne klasy throttle dla konkretnych widoków – np. surowszy limit na endpoint rejestracji/logaowania (żeby zapobiec brute-force na hasła), a inny na resztę.
Throttling korzysta z cache do przechowywania liczników, więc warto mieć skonfigurowany cache w Django (np. jak w poprzednim punkcie). W dużych systemach, oprócz throttle aplikacyjnego, czasem stosuje się też limitowanie na poziomie serwera www lub firewalla (np. limit requestów z jednego IP). Niemniej DRF Throttling umożliwia łatwo per-user i per-IP limity.
Przykład użycia per widok:
from rest_framework.throttling import UserRateThrottle
class BurstRateThrottle(UserRateThrottle):
rate = '5/min' # własna klasa limitu: 5 na minutę
class MyActionView(APIView):
throttle_classes = [BurstRateThrottle]
...
Tutaj dla konkretnego endpointu ustawiamy bardzo niski limit (np. operacja, która może obciążać system). Możemy też użyć wbudowanego ScopedRateThrottle
i nazwać throttlingi (przydatne przy wielu różnych limitach na różne grupy widoków).
W skrócie: włącz domyślny throttling chyba że masz pewność, że API jest zamknięte i kontrolujesz klientów. To zabezpieczy przed zalewaniem API żądaniami (celowymi lub przez błąd klienta). Ograniczenia ustal rozsądnie (np. 1000/dzień na usera to ~1 request na 1.5 minuty, co dla większości aplikacji użytkowych jest OK; można dać wyższe jeśli to aplikacja potrzebująca częstego odpytywania). Pamiętaj także o limitach dla niezalogowanych – bo atakujący może tworzyć ciągle nowe konta lub uderzać bez tokenu.

Bezpieczeństwo JSON Web Token (JWT):
Jeśli używasz JWT do uwierzytelniania, zastosuj się do następujących wskazówek:
- Używaj HTTPS – to zasada ogólna, ale tu szczególnie ważna. Token JWT niesiony w nagłówku może być przechwycony jeśli połączenie nie jest szyfrowane. HTTPS chroni token w tranzycie (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI).
- Krótka ważność tokenu dostępowego – standardowo tokeny dostępu (access token) ustawia się na krótki czas (np. 5, 10, 15 minut) (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI). Dzięki temu nawet jeśli ktoś przechwyci token (np. przez XSS, patrz niżej), ma małe okienko czasowe, by go użyć. Do odświeżania sesji służy refresh token – o dłuższej żywotności (np. 1 dzień lub tydzień).
- Bezpieczne przechowywanie tokenu po stronie klienta: Nie wolno JWT (ani żadnego tokenu sesyjnego) przechowywać w miejscach podatnych na ataki XSS, jak
localStorage
lub widoczne ciasteczko. Lepsze opcje:- HttpOnly cookie – przeglądarka sama wysyła, a JS nie ma do niego dostępu (utrudnia kradzież przez XSS). Należy ustawić SameSite=Lax/Strict, by ograniczyć CSRF.
- Przechowywanie w pamięci (in-memory) aplikacji frontend – token trzymany w zmiennej JS znika po odświeżeniu strony (ale jest to pewien trade-off między wygodą a bezpieczeństwem).
- W przypadku aplikacji mobilnych – przechowywać w bezpiecznym miejscu zapewnianym przez system (Keychain na iOS, SecureStorage/Keystore na Android).
- Nigdy nie umieszczaj tokenu w adresie URL (np. jako query param) – może wyciec w logach, refererach itp.
- Implementacja wylogowania / unieważniania tokenów: JWT są stateless, co oznacza że na serwerze nie trzyma się aktywnych tokenów. Aby jednak umożliwić unieważnienie (np. gdy user się wyloguje lub token zostanie skradziony), stosuje się tzw. blacklistę tokenów. Np.
djangorestframework-simplejwt
oferuje aplikacjętoken_blacklist
. Można też zmniejszać ważność refresh tokena i rotować go przy każdym użyciu (tzw. rotating refresh tokens), co ogranicza okno użycia skradzionego refresh tokena. Ważne, by w razie czego móc unieważnić token przed jego wygaśnięciem – np. zmuszając użytkownika do ponownego logowania przy podejrzanej aktywności. - Walidacja i obsługa tokenu po stronie serwera: Upewnij się, że weryfikujesz wszystkie standardowe claimy JWT: czy token nie wygasł (
exp
), czy jest wystawiony przez nas (iss
), czy ma poprawnego odbiorcę jeśli używany (aud
), etc. (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI). Biblioteki zazwyczaj robią to automatycznie, ale jeśli np. dodajesz własne claimy, też je sprawdzaj. Używaj silnych algorytmów (HS256+ z długim sekretem lub RS256 z kluczem prywatnym). Sekret JWT trzymaj w tajemnicy – najlepiej w zmiennej środowiskowej, nigdy w repozytorium (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI). - Zabezpieczenie przed XSS: Jeśli trzymasz token w pamięci przeglądarki lub localStorage, XSS (Cross-Site Scripting) pozwoli atakującemu odczytać token i użyć go. Obrona to dobre praktyki frontendu (Content Security Policy, unikanie niebezpiecznych konstrukcji eval/innerHTML, itp.), ewentualnie trzymanie tokenu w HttpOnly cookie (ale wtedy patrz punkt o CSRF). Nie ma idealnej ochrony, trzeba być świadomym ryzyk i je akceptować lub mitigować.
- Nie polegaj tylko na JWT wrażliwych operacji: Czasem, dla naprawdę krytycznych akcji (np. potwierdzenie operacji finansowej), sam fakt posiadania tokenu może być niewystarczający – można rozważyć drugie hasło, token SMS, itp. To poza zakresem DRF, ale warte wspomnienia.
Ogólnie JWT są bezpieczne, o ile poprawnie zaimplementowane – stosowane przez największe serwisy. Błędy implementacji (np. brak weryfikacji podpisu – zdarzało się, że ktoś akceptował algorytm "none") lub złe przechowywanie tokenów to główne wektory ataków. Trzymając się powyższych zasad, zapewnisz wysoki poziom bezpieczeństwa JWT auth w DRF (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI) (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI).
Ograniczenie pola widzenia i uprawnień:
Stosuj zasadę najmniejszych uprawnień również w kodzie:
- Jeśli endpoint ma służyć tylko do odczytu danych publicznych, użyj
IsAuthenticatedOrReadOnly
albo nawet AllowAny (gdy naprawdę nic wrażliwego tam nie ma), zamiast przypadkiem zezwalać na zapisy. - Ukrywaj wrażliwe pola w serializerach. Np. nigdy nie wystawiaj pola hasła. Jeśli z jakiegoś powodu musisz przekazać hash lub token jednorazowy, upewnij się, że to bezpieczne. Dobrym wzorcem jest mieć osobne serializery do operacji czytania i pisania, jeśli zakres pól się różni (DRF umożliwia np.
read_only_fields
lub całkiem różne serializer classes dla GET i dla POST). - Filtruj querysety według użytkownika tam, gdzie to potrzebne. Jeśli zapomnisz, że jakiś widok listuje wszystkie obiekty, a miał listować tylko np. obiekty danego użytkownika, możesz stworzyć poważny wyciek danych. Testy integracyjne na uprawnienia pomogą to wychwycić.
- Rozważ użycie mechanizmów Object level permissions (np. django-guardian) jeśli masz złożone scenariusze współdzielenia danych między użytkownikami.
Bezpieczne nagłówki i CORS:
Jeśli API ma być dostępne z innych domen (np. frontend w JS na innej domenie), skonfiguruj poprawnie CORS (Cross-Origin Resource Sharing). Użyj biblioteki django-cors-headers – dodaj ją i ustaw CORS_ALLOWED_ORIGINS
na listę zaufanych domen. Nie ustawiaj CORS_ALLOW_ALL_ORIGINS = True
w produkcji, bo to pozwala dowolnej stronie robić żądania do twojego API (chyba że to publiczne, otwarte API, ale i wtedy lepiej kontrolować kto korzysta). CORS sam w sobie nie jest zabezpieczeniem, ale jego zła konfiguracja może otworzyć na CSRF lub pozwolić nieautoryzowanym stronom korzystać z Twojego API przez przeglądarkę użytkownika.
Z drugiej strony, pamiętaj że CORS to tylko mechanizm przeglądarki – nie chroni przed tym, że ktoś napisze skrypt cURL, aplikację mobilną czy użyje requests
z innej domeny. Stąd kluczowe jest prawidłowe uwierzytelnianie i autoryzacja na poziomie API. CORS zapobiega pewnej klasie ataków CSRF (międzydomenowych), ale to nie panaceum.
Django Security Best Practices:
Nie zapominaj, że DRF siedzi na Django – więc wszystkie ogólne zabezpieczenia też obowiązują:
- Wyłącz DEBUG na produkcji.
- Ustaw poprawnie ALLOWED_HOSTS.
- Korzystaj z HTTPS (SECURE_SSL_REDIRECT, SECURE_PROXY_SSL_HEADER itd. jeśli za proxy).
- Rozważ ustawienia HSTS, Secure cookie, itp. (częściowo to rola serwera www / proxy).
- Aktualizuj na bieżąco Django, DRF i zależności, by mieć najnowsze poprawki bezpieczeństwa (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI). W szczególności często pojawiają się łatki do potencjalnych vektorów XSS w przeglądarce do przeglądania API DRF – jeśli jej nie używasz na produkcji, możesz w ogóle wyłączyć
BrowsableAPIRenderer
zostawiając tylko JSONRenderer, co bywa zalecane dla bezpieczeństwa.
Monitoring i logi:
W dużej aplikacji warto wdrożyć logowanie zdarzeń bezpieczeństwa:
- Loguj nieudane logowania, próby dostępu do niedozwolonych zasobów (np.
PermissionDenied
). - Monitoruj te logi pod kątem wielu nieudanych prób (może wskazywać atak brute-force, wtedy reaguj – np. zablokuj adresy IP lub konta chwilowo, choć DRF throttling częściowo to załatwi).
- Loguj ważne akcje użytkowników (np. zmiana hasła, usunięcie konta) – audyt przydaje się gdy coś się wydarzy, by odtworzyć sekwencję zdarzeń.
Common vulnerabilities w API i jak im zapobiegać:
- SQL Injection: używanie ORM właściwie chroni przed SQL injection (zapytania są kompilowane z parametrami). Unikaj jednak składania raw SQL z niezaufanych danych. Jeśli musisz użyć raw SQL (mało prawdopodobne w DRF), parametryzuj zapytania.
- XSS: DRF zwraca JSON, więc klasyczne XSS na stronach HTML nas nie dotyczy. Ale jeżeli gdzieś przekazujemy dane użytkownika w renderowanych stronach (np. w docstringach generujących HTML w browsable API), to standardowe mechanizmy Django (escapowanie) obowiązują.
- Insecure Direct Object References (IDOR): to sytuacja, gdy API pozwala odwołać się do obiektu po id, ale nie sprawdza, czy należy do użytkownika. Np. użytkownik znając cudze /api/orders/5 może pobrać zamówienie, które nie jest jego. Zapobiegamy temu poprzez filtrowanie querysetu odpowiednio (np.
queryset = Order.objects.filter(user=request.user)
) albo użycie object permissions. Testy bezpieczeństwa powinny sprawdzać, czy nie można uzyskać dostępu do nie-swoich danych. - Mass Assignment: w DRF potencjalnie można by masowo ustawić pola, których nie chcemy. Np. użytkownik przy rejestracji może próbować ustawić is_staff=true. DRF ModelSerializer domyślnie używa wszystkich pól modelu, chyba że ograniczymy
fields
. Zawsze jawnie wskazuj, jakie pola API przyjmuje, zamiastfields = '__all__'
dla modeli użytkownika czy wrażliwych – żeby nie dopuścić do nadpisania czegoś, czego nie przewidzieliśmy. Można też skorzystać zExtra_kwargs
w serializerze, np. markis_staff
jako read_only. - Brak limitów/potwierdzeń przy operacjach krytycznych: np. brak limitu prób hasła (już omówione – throttle), brak dwuetapowej weryfikacji przy zmianie ważnych danych (np. email, numer telefonu – można rozważyć wysyłanie potwierdzenia).
- Bezpieczeństwo plików: Jeśli API umożliwia upload plików, upewnij się, że korzystasz z bezpiecznych metod (Storage zapewnia to w dużym stopniu), nie pozwól zapisać pliku np. z
.php
na serwerze itd. To bardziej kwestia konfiguracji serwera i Django (np. default Media root nie jest wykonywalna).
Na koniec – świadomie testuj bezpieczeństwo. Warto wykonać testy penetracyjne API, sprawdzić narzędziami typu OWASP ZAP czy choćby spróbować "oszukać" API ręcznie (np. zmieniając ID w URL, pola JSON) – lepiej, żeby programista znalazł lukę, niż miałby to zrobić atakujący. W dużych organizacjach stosuje się też bug bounty lub dedykowane audyty bezpieczeństwa dla API.
Podsumowując: bezpieczeństwo w DRF opiera się na solidnych fundamentach Django (które jest dość bezpieczne domyślnie) oraz na stosowaniu powyższych praktyk: ograniczaj dostęp (auth/permissions), waliduj dane (Serializers to robią za nas w sporym zakresie), chroń się przed nadużyciami (CSRF, throttling) i zabezpiecz kanały komunikacji (HTTPS, prawidłowa konfiguracja CORS). Dbaj o aktualizacje i testy. Dzięki temu Twoje API będzie odporne na większość typowych zagrożeń, a użytkownicy i ich dane – bezpieczni (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI) (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI).
Źródła: Dokumentacja Django REST Framework oraz doświadczenia z praktycznych wdrożeń. Powołania na fragmenty dokumentacji i artykułów w tekście wskazują na konkretyczne zalecenia i przykłady: optymalizacja zapytań (Serializer relations - Django REST framework) (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English), użycie uprawnień modelowych (Permissions - Django REST framework), konfiguracja paginacji (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English), integracja django-filter (Integration with DRF - django-filter 25.1 documentation), cachowanie widoków (TOP 7 Advanced Techniques for Optimizing Django REST Framework APIs in 2024 | by Samuel Getachew | Python in Plain English), przykładowy test APITestCase (Testing - Django REST framework), konfiguracja drf-yasg (drf-yasg - Yet another Swagger generator — drf-yasg 1.21.7 documentation) i drf-spectacular (drf-spectacular - drf-spectacular documentation), oraz zalecenia bezpieczeństwa (rate limiting, JWT) (How to throttle your API with Django Rest Framework - DEV Community) (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI) (Implementing JWT authentication in Django: A comprehensive Guide | UnfoldAI). Wszystkie te fragmenty wspierają powyższe dobre praktyki konkretnymi odniesieniami do uznanych źródeł. Bezpieczeństwo, wydajność i dobre utrzymanie API to ciągły proces, ale powyższe punkty stanowią solidną podstawę dla każdego projektu opartego o Django REST Framework.