Django REST Framework: руководство по созданию API
Как создать REST API на Django с DRF: установка, сериализаторы, ViewSet и роутеры, аутентификация, права, пагинация, фильтры, Swagger и тесты.
Содержание · 16 разделов
- Установка DRF и настройка проекта
- Модели для примера
- Сериализаторы: Serializer и ModelSerializer
- Serializer — всё вручную
- ModelSerializer — поля из модели
- Представления: от @api_view до ViewSet
- Функция с @api_view
- APIView
- Generic-представления
- ViewSet и роутер
- Аутентификация в API
- TokenAuthentication
- JWT через Simple JWT
- Права доступа (permissions)
- Пагинация
- Фильтрация, поиск и сортировка
- Производительность: N+1 в сериализаторах
- Документация API: drf-spectacular и Swagger
- Тестирование API: APITestCase и APIClient
- Django Ninja — альтернатива DRF
- Частые ошибки
- 403 «CSRF Failed» при POST из браузера
- 403 вместо 401 для анонимного пользователя
- AssertionError: basename argument not specified
- UnorderedObjectListWarning при пагинации
- Поиск не находит кириллицу без учёта регистра
- Фронтенд на другом домене получает ошибку CORS
- Чек-лист API на DRF
- Частые вопросы
- Как изменить время жизни JWT-токенов?
- Как сделать выход из системы с JWT?
- Как отдавать разные поля в списке и в детальном просмотре?
- Как ограничить частоту запросов к API?
- Как добавить версию в URL API?
- Итог
Django REST Framework (DRF) — библиотека для создания REST API на Django. Она добавляет к фреймворку сериализаторы (преобразование моделей в JSON и обратно с валидацией), классы представлений для API, роутеры, аутентификацию, права доступа, пагинацию и веб-интерфейс для ручной проверки эндпоинтов.
Быстрый ответ: pip install djangorestframework, добавьте "rest_framework" в INSTALLED_APPS, опишите ModelSerializer и ModelViewSet и зарегистрируйте ViewSet в DefaultRouter — получится полноценный CRUD API примерно в 30 строк. Ниже разобраны все этапы и то, что понадобится дальше: права, токены, фильтры, документацию Swagger и тесты. Примеры проверены на Django 5.2 и 6.x с DRF 3.x.
Установка DRF и настройка проекта
Понадобится проект Django с приложением. Если его ещё нет — установите Django в виртуальное окружение и создайте проект. Затем установите DRF:
python -m pip install djangorestframework
Подключите приложение в settings.py:
# config/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"library", # ваше приложение
]
Глобальные настройки DRF собраны в одном словаре REST_FRAMEWORK. На старте достаточно задать права по умолчанию и пагинацию — остальные ключи разберём по ходу статьи:
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
Важно: если
DEFAULT_PERMISSION_CLASSESне указан, DRF используетAllowAny— любой анонимный клиент сможет создавать, менять и удалять записи. Задавайте права по умолчанию сразу.
Добавьте в корневой urls.py вход для веб-интерфейса API — он пригодится для ручной проверки:
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("library.urls")),
path("api-auth/", include("rest_framework.urls")),
]
Модели для примера
Все примеры используют книжный каталог: автор, книга и владелец записи — текущий пользователь. Подробно о полях и Meta — в статье про модели Django.
# library/models.py
from django.conf import settings
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=200)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.PROTECT, related_name="books")
published = models.DateField()
price = models.DecimalField(max_digits=8, decimal_places=2)
is_available = models.BooleanField(default=True)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="books"
)
class Meta:
ordering = ["-published"]
def __str__(self):
return self.title
python manage.py makemigrations library
python manage.py migrate
Сериализаторы: Serializer и ModelSerializer
Сериализатор делает две вещи: превращает объект модели в словарь для JSON (serializer.data) и проверяет входящие данные перед сохранением (serializer.is_valid()).
Serializer — всё вручную
Базовый Serializer похож на обычную форму Django: поля объявляются явно, а сохранение описывается методами create() и update().
from rest_framework import serializers
from .models import Book
class BookPlainSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=300)
price = serializers.DecimalField(max_digits=8, decimal_places=2)
def create(self, validated_data):
return Book.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.title = validated_data.get("title", instance.title)
instance.price = validated_data.get("price", instance.price)
instance.save()
return instance
ModelSerializer — поля из модели
В большинстве случаев удобнее ModelSerializer: он строит поля и валидаторы по модели и уже умеет create()/update().
# library/serializers.py
from rest_framework import serializers
from .models import Author, Book
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ["id", "name"]
class BookSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.name", read_only=True)
owner = serializers.ReadOnlyField(source="owner.username")
class Meta:
model = Book
fields = [
"id", "title", "author", "author_name",
"published", "price", "is_available", "owner",
]
read_only_fields = ["is_available"]
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("Цена должна быть больше нуля.")
return value
Что здесь происходит:
fields— явный список полей. Не используйтеfields = "__all__"в публичном API: новое поле модели автоматически попадёт в ответ.source="author.name"берёт значение по связи;read_only=Trueисключает поле из входных данных.validate_<поле>()проверяет одно поле,validate(self, attrs)— несколько полей сразу.
Проверим сериализатор в python manage.py shell:
>>> from library.models import Book
>>> from library.serializers import BookSerializer
>>> BookSerializer(Book.objects.first()).data
{'id': 1, 'title': 'Война и мир', 'author': 1, 'author_name': 'Лев Толстой',
'published': '1869-01-01', 'price': '990.00', 'is_available': True, 'owner': 'anna'}
>>> s = BookSerializer(data={"title": "Черновик", "author": 1, "published": "2026-01-01", "price": "0"})
>>> s.is_valid()
False
>>> s.errors
{'price': [ErrorDetail(string='Цена должна быть больше нуля.', code='invalid')]}
Внешний ключ по умолчанию выводится как ID (PrimaryKeyRelatedField). Чтобы отдать связанный объект целиком, вложите другой сериализатор:
class BookNestedSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
class Meta:
model = Book
fields = ["id", "title", "author"]
# → {'id': 1, 'title': 'Война и мир', 'author': {'id': 1, 'name': 'Лев Толстой'}}
DRF не сохраняет вложенные данные автоматически: если вложенный сериализатор должен принимать данные на запись, переопределите create() и update() родительского сериализатора.
Представления: от @api_view до ViewSet
DRF предлагает несколько уровней абстракции. Чем выше уровень, тем меньше кода, но тем больше поведения скрыто «под капотом».
| Уровень | Что пишете вы | Когда использовать |
|---|---|---|
@api_view | функцию на каждый эндпоинт | простые эндпоинты, не привязанные к модели |
APIView | методы get(), post() и т. д. | нестандартная логика, полный контроль |
generics (ListCreateAPIView и др.) | queryset и serializer_class | типовой CRUD с отдельными URL |
ModelViewSet + роутер | один класс на ресурс | полный CRUD, единообразные URL |
Функция с @api_view
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(["GET"])
def ping(request):
return Response({"status": "ok"})
Response сам выбирает формат ответа (JSON или HTML-интерфейс в браузере), а request.data содержит уже разобранное тело запроса — JSON, форму или multipart.
APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
class BookListAPIView(APIView):
def get(self, request):
books = Book.objects.select_related("author", "owner")
serializer = BookSerializer(books, many=True)
return Response(serializer.data)
def post(self, request):
serializer = BookSerializer(data=request.data)
serializer.is_valid(raise_exception=True) # при ошибке — ответ 400
serializer.save(owner=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
Generic-представления
Generic-классы уже содержат логику списка, создания, чтения, обновления и удаления — остаётся указать данные:
from rest_framework import generics
class BookListCreate(generics.ListCreateAPIView):
queryset = Book.objects.select_related("author", "owner")
serializer_class = BookSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class BookDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Book.objects.select_related("author", "owner")
serializer_class = BookSerializer
# library/urls.py
urlpatterns = [
path("books/", views.BookListCreate.as_view()),
path("books/<int:pk>/", views.BookDetail.as_view()),
]
perform_create() — правильное место, чтобы подставить данные, которых нет в запросе: владельца, текущую дату, IP.
ViewSet и роутер
ModelViewSet объединяет все шесть действий (list, create, retrieve, update, partial_update, destroy) в одном классе, а роутер сам генерирует URL.
# library/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.select_related("author", "owner")
serializer_class = BookSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
@action(detail=True, methods=["post"])
def unpublish(self, request, pk=None):
book = self.get_object()
book.is_available = False
book.save(update_fields=["is_available"])
return Response({"status": "снята с продажи"})
# library/urls.py
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register("books", views.BookViewSet, basename="book")
urlpatterns = [
path("", include(router.urls)),
]
Роутер создаст маршруты:
| URL | Методы | Имя маршрута |
|---|---|---|
/api/ | GET (корень API) | api-root |
/api/books/ | GET, POST | book-list |
/api/books/<pk>/ | GET, PUT, PATCH, DELETE | book-detail |
/api/books/<pk>/unpublish/ | POST | book-unpublish |
Декоратор @action добавляет нестандартные действия: detail=True — для одного объекта, detail=False — для коллекции. Если нужен только просмотр, наследуйтесь от ReadOnlyModelViewSet.
Запустите python manage.py runserver и откройте http://127.0.0.1:8000/api/books/ в браузере: DRF покажет веб-интерфейс (Browsable API) с формой для POST-запросов. Вход для него — по ссылке «Log in» через api-auth/.
Аутентификация в API
Аутентификация отвечает на вопрос «кто делает запрос» и заполняет request.user. По умолчанию DRF включает SessionAuthentication и BasicAuthentication. Для мобильных и SPA-клиентов обычно нужны токены.
| Класс | Как передаются данные | Где уместен |
|---|---|---|
SessionAuthentication | cookie сессии Django + CSRF-токен | фронтенд на том же домене, Browsable API |
BasicAuthentication | логин и пароль в каждом запросе | только тесты и внутренние скрипты по HTTPS |
TokenAuthentication | заголовок Authorization: Token <ключ> | простые клиенты, один бессрочный токен |
JWTAuthentication (пакет djangorestframework-simplejwt) | заголовок Authorization: Bearer <access> | SPA и мобильные приложения |
TokenAuthentication
Встроенные токены хранятся в базе, поэтому нужно приложение rest_framework.authtoken и миграция:
INSTALLED_APPS += ["rest_framework.authtoken"]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
# ...
}
# config/urls.py
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns += [path("api/token-auth/", obtain_auth_token)]
python manage.py migrate
curl -X POST http://127.0.0.1:8000/api/token-auth/ -d "username=anna&password=s3cret-pass"
# → {"token":"9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"}
curl http://127.0.0.1:8000/api/books/ -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
JWT через Simple JWT
Для JWT стандартно используют пакет djangorestframework-simplejwt:
python -m pip install djangorestframework-simplejwt
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
}
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns += [
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]
Добавлять rest_framework_simplejwt в INSTALLED_APPS для такой схемы не нужно: это требуется только для переводов сообщений об ошибках и для приложения чёрного списка токенов (о нём — в частых вопросах ниже).
POST /api/token/ с логином и паролем возвращает пару access и refresh. Короткоживущий access передаётся в заголовке Authorization: Bearer ..., а refresh обменивается на новый access через /api/token/refresh/.
Пользователи, пароли и вход на сайт устроены в Django через django.contrib.auth — общая картина в руководстве по аутентификации в Django.
Права доступа (permissions)
Права отвечают на вопрос «можно ли этому пользователю это действие». Их задают глобально в DEFAULT_PERMISSION_CLASSES или для конкретного представления через permission_classes.
| Класс | Кого пускает |
|---|---|
AllowAny | всех (значение по умолчанию) |
IsAuthenticated | только вошедших пользователей |
IsAuthenticatedOrReadOnly | читать — всех, изменять — вошедших |
IsAdminUser | пользователей с is_staff=True |
DjangoModelPermissions | по правам модели add/change/delete из django.contrib.auth |
Типичная задача — разрешить изменение только автору записи. Для этого пишут свой класс с has_object_permission():
# library/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""Изменять объект может только его владелец."""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS: # GET, HEAD, OPTIONS
return True
return obj.owner == request.user
class BookViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
# ...
Чужой пользователь при попытке PATCH получит 403 с {"detail": "You do not have permission to perform this action."}.
Важно:
has_object_permission()вызывается изget_object(). В generic-представлениях и ViewSet это происходит автоматически, а в своёмAPIViewнужно вызватьself.check_object_permissions(request, obj)самостоятельно. Для списков объектные права не проверяются — ограничивайте выборку вget_queryset().
Пагинация
С настройками из начала статьи (PageNumberPagination, PAGE_SIZE: 20) ответ списка выглядит так:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"title": "Война и мир",
"author": 1,
"author_name": "Лев Толстой",
"published": "1869-01-01",
"price": "990.00",
"is_available": true,
"owner": "anna"
}
]
}
Страницы запрашиваются параметром ?page=2. Несуществующая страница вернёт 404 с {"detail": "Invalid page."}. Другие встроенные классы:
LimitOffsetPagination— параметры?limit=20&offset=40;CursorPagination— непрозрачный курсор вместо номера страницы; стабилен при вставке новых записей и подходит для лент. По умолчанию сортирует по полю-created, поэтому в подклассе пагинатора задайте атрибутorderingс существующим полем, например"-published".
Чтобы клиент мог менять размер страницы, создайте подкласс:
from rest_framework.pagination import PageNumberPagination
class StandardPagination(PageNumberPagination):
page_size = 20
page_size_query_param = "page_size"
max_page_size = 100
Фильтрация, поиск и сортировка
Для фильтров по полям установите django-filter, а поиск и сортировка встроены в DRF:
python -m pip install django-filter
INSTALLED_APPS += ["django_filters"]
REST_FRAMEWORK = {
# ...
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
}
class BookViewSet(viewsets.ModelViewSet):
# ...
filterset_fields = ["author", "is_available"]
search_fields = ["title", "author__name"]
ordering_fields = ["published", "price"]
Теперь работают запросы:
/api/books/?is_available=false — точный фильтр по полю
/api/books/?author=1 — по внешнему ключу
/api/books/?search=война — поиск по title и имени автора (icontains)
/api/books/?ordering=-price — сортировка, минус — по убыванию
Для произвольной логики переопределите get_queryset() и читайте параметры из self.request.query_params. Все приёмы фильтрации QuerySet собраны в шпаргалке по Django ORM.
Производительность: N+1 в сериализаторах
Поля вроде author_name и owner обращаются к связанным моделям. Если в queryset нет select_related, сериализатор делает отдельный запрос на каждую строку. Замер на 11 книгах:
from django.db import connection
from django.test.utils import CaptureQueriesContext
with CaptureQueriesContext(connection) as ctx:
BookSerializer(Book.objects.all(), many=True).data
print(len(ctx.captured_queries)) # → 23: 1 на книги + 11 на авторов + 11 на владельцев
with CaptureQueriesContext(connection) as ctx:
BookSerializer(Book.objects.select_related("author", "owner"), many=True).data
print(len(ctx.captured_queries)) # → 1
Для внешних ключей используйте select_related, для обратных связей и ManyToMany — prefetch_related. Подробный разбор — в статье про N+1 в Django ORM.
Документация API: drf-spectacular и Swagger
drf-spectacular генерирует схему OpenAPI 3 по вашим сериализаторам и представлениям и подключает Swagger UI и ReDoc.
python -m pip install drf-spectacular
INSTALLED_APPS += ["drf_spectacular"]
REST_FRAMEWORK = {
# ...
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "Library API",
"DESCRIPTION": "API книжного каталога",
"VERSION": "1.0.0",
}
# config/urls.py
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
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"),
]
Swagger UI откроется по адресу /api/docs/. Схему можно сохранить в файл и проверить в CI:
python manage.py spectacular --file schema.yml --validate
Для generic-представлений и ViewSet схема строится автоматически. Для @api_view и APIView spectacular не может угадать сериализатор и пишет предупреждение unable to guess serializer — подскажите его декоратором @extend_schema:
from drf_spectacular.utils import extend_schema
class BookListAPIView(APIView):
@extend_schema(responses=BookSerializer(many=True))
def get(self, request):
...
@extend_schema(request=BookSerializer, responses={201: BookSerializer})
def post(self, request):
...
Тестирование API: APITestCase и APIClient
DRF расширяет тестовые инструменты Django: APITestCase использует APIClient, который умеет отправлять JSON (format="json") и обходить аутентификацию через force_authenticate().
# library/tests.py
from datetime import date
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Author, Book
User = get_user_model()
class BookAPITests(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user("anna", password="s3cret-pass")
cls.author = Author.objects.create(name="Лев Толстой")
cls.book = Book.objects.create(
title="Война и мир",
author=cls.author,
published=date(1869, 1, 1),
price="990.00",
owner=cls.user,
)
def test_list_is_public(self):
response = self.client.get(reverse("book-list"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 1)
def test_anonymous_cannot_create(self):
response = self.client.post(reverse("book-list"), {}, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_owner_creates_book(self):
self.client.force_authenticate(user=self.user)
payload = {
"title": "Анна Каренина",
"author": self.author.pk,
"published": "1878-01-01",
"price": "750.00",
}
response = self.client.post(reverse("book-list"), payload, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["owner"], "anna")
def test_list_queries(self):
with self.assertNumQueries(2): # COUNT для пагинации + SELECT с JOIN
self.client.get(reverse("book-list"))
python manage.py test library
# → Ran 4 tests in 0.549s
# → OK
Тест assertNumQueries защищает от регрессий N+1: если кто-то уберёт select_related, тест упадёт. Для токенов используйте self.client.credentials(HTTP_AUTHORIZATION="Token " + key).
Django Ninja — альтернатива DRF
Django Ninja — библиотека для API на Django в стиле FastAPI: эндпоинты описываются функциями с аннотациями типов, валидация идёт через Pydantic, документация OpenAPI генерируется автоматически.
# library/api.py
from django.shortcuts import get_object_or_404
from ninja import ModelSchema, NinjaAPI
from .models import Book
api = NinjaAPI()
class BookOut(ModelSchema):
class Meta:
model = Book
fields = ["id", "title", "price", "published"]
@api.get("/books", response=list[BookOut])
def list_books(request):
return Book.objects.all()
@api.get("/books/{book_id}", response=BookOut)
def get_book(request, book_id: int):
return get_object_or_404(Book, id=book_id)
# config/urls.py
from library.api import api
urlpatterns += [path("ninja/", api.urls)] # Swagger UI — /ninja/docs
Когда что выбирать:
- DRF — зрелая экосистема (фильтры, права, JWT, вложенные роутеры), много готовых решений и специалистов; оптимален для CRUD вокруг моделей.
- Django Ninja — меньше кода, подсказки типов и Pydantic; удобен, если команда знакома с FastAPI или API небольшой.
Роутеры, схемы, аутентификация, пагинация и асинхронные эндпоинты на Ninja разобраны в статье «Django Ninja: API на Django с аннотациями типов».
Если Django как таковой не нужен, сравните варианты в обзоре FastAPI, Django и Flask.
Частые ошибки
403 «CSRF Failed» при POST из браузера
SessionAuthentication требует CSRF-токен для небезопасных методов, если пользователь вошёл через сессию. Передавайте токен в заголовке X-CSRFToken (значение из cookie csrftoken) или используйте токены/JWT для внешних клиентов.
403 вместо 401 для анонимного пользователя
Код ответа зависит от первого класса в DEFAULT_AUTHENTICATION_CLASSES. SessionAuthentication не отдаёт заголовок WWW-Authenticate, поэтому DRF возвращает 403. Если первым стоит TokenAuthentication, тот же запрос получит 401 с WWW-Authenticate: Token.
AssertionError: basename argument not specified
Роутер берёт имя маршрута из атрибута queryset. Если вы его не задали и переопределили только get_queryset(), передайте basename явно: router.register("books", BookViewSet, basename="book").
UnorderedObjectListWarning при пагинации
Пагинатор предупреждает, что QuerySet без сортировки может давать пропуски и дубли между страницами. Задайте ordering в Meta модели или вызовите .order_by() в queryset.
Поиск не находит кириллицу без учёта регистра
На SQLite icontains не учитывает регистр только для ASCII: ?search=война не найдёт «Война и мир». На PostgreSQL поиск работает корректно — используйте её и в разработке.
Фронтенд на другом домене получает ошибку CORS
Браузер блокирует запросы между доменами. Установите django-cors-headers, добавьте "corsheaders" в INSTALLED_APPS, "corsheaders.middleware.CorsMiddleware" — в начало MIDDLEWARE (выше CommonMiddleware) и перечислите разрешённые источники в CORS_ALLOWED_ORIGINS.
Чек-лист API на DRF
- задан
DEFAULT_PERMISSION_CLASSES— не остаётсяAllowAnyпо умолчанию - в сериализаторах явный список
fields, служебные поля —read_only - владелец и другие серверные данные подставляются в
perform_create() - у
querysetестьselect_related/prefetch_relatedпод поля сериализатора - включена пагинация, у QuerySet задана сортировка
- для внешних клиентов — токены или JWT, для своего фронтенда — сессия с CSRF
- схема OpenAPI генерируется без ошибок (
spectacular --validate) - тесты на права и
assertNumQueriesдля списков
Частые вопросы
Как изменить время жизни JWT-токенов?
По умолчанию в Simple JWT access живёт 5 минут, refresh — сутки. Значения задаются в словаре SIMPLE_JWT:
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
}
Короткий access ограничивает ущерб от утечки токена, длинный refresh избавляет пользователя от частого ввода пароля.
Как сделать выход из системы с JWT?
JWT проверяется по подписи, и сервер не может «забыть» уже выданный access до истечения срока. Выход сводится к тому, что клиент удаляет токены, а сервер заносит refresh в чёрный список. Для этого подключите приложение rest_framework_simplejwt.token_blacklist, выполните migrate и добавьте эндпоинт:
INSTALLED_APPS += ["rest_framework_simplejwt.token_blacklist"]
from rest_framework_simplejwt.views import TokenBlacklistView
urlpatterns += [path("api/token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist")]
После POST с {"refresh": "..."} обновить этот токен уже нельзя: /api/token/refresh/ ответит 401 с Token is blacklisted. С настройками ROTATE_REFRESH_TOKENS и BLACKLIST_AFTER_ROTATION старый refresh попадает в список и при каждом обновлении.
Как отдавать разные поля в списке и в детальном просмотре?
Переопределите get_serializer_class() и выберите сериализатор по self.action:
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.select_related("author", "owner")
def get_serializer_class(self):
if self.action == "list":
return BookListSerializer # id и title
return BookSerializer # все поля
Так список остаётся лёгким, а подробности приходят только для одного объекта.
Как ограничить частоту запросов к API?
Включите встроенный throttling: лимиты задаются отдельно для анонимов и вошедших пользователей.
REST_FRAMEWORK = {
# ...
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {"anon": "100/hour", "user": "1000/day"},
}
При превышении клиент получает 429 с {"detail": "Request was throttled. Expected available in 59 seconds."}. Счётчики хранятся в кеше Django, поэтому при нескольких процессах нужен общий кеш, например Redis.
Как добавить версию в URL API?
Проще всего — префикс в urls.py: path("api/v1/", include(router.urls)). Когда понадобится вторая версия, сделайте версию параметром маршрута и включите URLPathVersioning:
# config/urls.py
urlpatterns += [path("api/<str:version>/", include(router.urls))]
# settings.py
REST_FRAMEWORK = {
# ...
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"ALLOWED_VERSIONS": ["v1", "v2"],
}
В представлениях версия доступна как request.version — по ней выбирают сериализатор или логику. Запрос к версии вне списка получит 404 с Invalid version in URL path.
Итог
DRF превращает модели Django в REST API за счёт трёх слоёв: сериализаторы описывают данные, ViewSet и роутеры — эндпоинты, а классы аутентификации, прав и пагинации настраиваются в одном словаре REST_FRAMEWORK. Начните с ModelSerializer и ModelViewSet, сразу задайте права и пагинацию, следите за количеством запросов и подключите drf-spectacular для документации.
Дальше: разберитесь с моделями Django, оптимизируйте запросы по статье про N+1 и настройте пользователей по руководству «Аутентификация в Django». Все статьи раздела — в хабе REST API на Django. Полный справочник — в официальной документации DRF.
- #Django REST Framework
- #DRF
- #REST API
- #Сериализаторы
- #ViewSet
- #drf-spectacular