Django-Admin.ru
Django · REST API★ Руководство

Django REST Framework: руководство по созданию API

Как создать REST API на Django с DRF: установка, сериализаторы, ViewSet и роутеры, аутентификация, права, пагинация, фильтры, Swagger и тесты.

Редакция Django-Admin.ru
· обновлено · 18 мин чтения

Содержание · 16 разделов
  1. Установка DRF и настройка проекта
  2. Модели для примера
  3. Сериализаторы: Serializer и ModelSerializer
  4. Serializer — всё вручную
  5. ModelSerializer — поля из модели
  6. Представления: от @api_view до ViewSet
  7. Функция с @api_view
  8. APIView
  9. Generic-представления
  10. ViewSet и роутер
  11. Аутентификация в API
  12. TokenAuthentication
  13. JWT через Simple JWT
  14. Права доступа (permissions)
  15. Пагинация
  16. Фильтрация, поиск и сортировка
  17. Производительность: N+1 в сериализаторах
  18. Документация API: drf-spectacular и Swagger
  19. Тестирование API: APITestCase и APIClient
  20. Django Ninja — альтернатива DRF
  21. Частые ошибки
  22. 403 «CSRF Failed» при POST из браузера
  23. 403 вместо 401 для анонимного пользователя
  24. AssertionError: basename argument not specified
  25. UnorderedObjectListWarning при пагинации
  26. Поиск не находит кириллицу без учёта регистра
  27. Фронтенд на другом домене получает ошибку CORS
  28. Чек-лист API на DRF
  29. Частые вопросы
  30. Как изменить время жизни JWT-токенов?
  31. Как сделать выход из системы с JWT?
  32. Как отдавать разные поля в списке и в детальном просмотре?
  33. Как ограничить частоту запросов к API?
  34. Как добавить версию в URL API?
  35. Итог

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, POSTbook-list
/api/books/<pk>/GET, PUT, PATCH, DELETEbook-detail
/api/books/<pk>/unpublish/POSTbook-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-клиентов обычно нужны токены.

КлассКак передаются данныеГде уместен
SessionAuthenticationcookie сессии 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
↑ ↓ выборEnter открытьEsc закрыть