Создание API на Django REST Framework

Создание API на Django REST Framework REST API

Привет! Django REST Framework (DRF) — это мощный и гибкий инструмент для создания веб-API на Django. Он предоставляет готовые решения для сериализации, аутентификации, маршрутизации и многого другого.

В этой статье мы создадим полноценное REST API для управления книгами.

В этой статье мы разберём:

  • Установку и настройку DRF
  • Создание моделей
  • Сериализаторы
  • ViewSet и маршрутизацию
  • Browsable API

Что нужно знать перед началом

Для этого урока тебе понадобится:

  • Установленный Django (4.2 или новее)
  • Базовое понимание моделей и URL

Совет: DRF автоматически генерирует удобную документацию — Browsable API, доступную прямо в браузере.

Основная часть

Установка и настройка

Шаг 1. Установка DRF:

pip install djangorestframework

Шаг 2. Добавь приложение в settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    # ...
    'rest_framework',      # Добавляем DRF
    'books',               # Наше приложение
]

Шаг 3. Создание проекта и приложения:

django-admin startproject bookstore
cd bookstore
python manage.py startapp books

Шаг 4. Настройка DRF (опционально):

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
}

Совет: IsAuthenticatedOrReadOnly позволяет читать данные без авторизации, но требует входа для создания/изменения.

Создание моделей

books/models.py:

from django.db import models
from django.contrib.auth.models import User

class Author(models.Model):
    name = models.CharField(max_length=200)
    bio = models.TextField(blank=True)
    birth_date = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name='books'
    )
    isbn = models.CharField(max_length=13, unique=True)
    description = models.TextField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    published_date = models.DateField()
    in_stock = models.BooleanField(default=True)
    created_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        related_name='created_books'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"{self.title} by {self.author.name}"

class Review(models.Model):
    book = models.ForeignKey(
        Book,
        on_delete=models.CASCADE,
        related_name='reviews'
    )
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='reviews'
    )
    rating = models.IntegerField(choices=[(i, i) for i in range(1, 6)])
    comment = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ['book', 'user']  # Один отзыв на пользователя на книгу

    def __str__(self):
        return f"Review by {self.user.username}"

Миграции:

python manage.py makemigrations
python manage.py migrate

Создание сериализаторов

Сериализаторы преобразуют модели в JSON и обратно.

books/serializers.py:

from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Author, Book, Review

class AuthorSerializer(serializers.ModelSerializer):
    book_count = serializers.SerializerMethodField()

    class Meta:
        model = Author
        fields = ['id', 'name', 'bio', 'birth_date', 'book_count', 'created_at']
        read_only_fields = ['created_at']

    def get_book_count(self, obj):
        return obj.books.count()

class ReviewSerializer(serializers.ModelSerializer):
    user = serializers.StringRelatedField(read_only=True)

    class Meta:
        model = Review
        fields = ['id', 'book', 'user', 'rating', 'comment', 'created_at']
        read_only_fields = ['user', 'created_at']

class BookListSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author.name', read_only=True)

    class Meta:
        model = Book
        fields = ['id', 'title', 'author_name', 'price', 'in_stock']

class BookDetailSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)
    author_id = serializers.PrimaryKeyRelatedField(
        queryset=Author.objects.all(),
        source='author',
        write_only=True
    )
    reviews = ReviewSerializer(many=True, read_only=True)
    created_by = serializers.StringRelatedField(read_only=True)

    class Meta:
        model = Book
        fields = [
            'id', 'title', 'author', 'author_id', 'isbn',
            'description', 'price', 'published_date', 'in_stock',
            'reviews', 'created_by', 'created_at', 'updated_at'
        ]
        read_only_fields = ['created_by', 'created_at', 'updated_at']

    def validate_isbn(self, value):
        clean_isbn = value.replace('-', '')
        if len(clean_isbn) not in [10, 13]:
            raise serializers.ValidationError("ISBN must be 10 or 13 digits")
        if not clean_isbn.isdigit():
            raise serializers.ValidationError("ISBN must contain only digits")
        return clean_isbn

    def validate_price(self, value):
        if value <= 0:
            raise serializers.ValidationError("Price must be greater than zero")
        return value

Совет: ModelSerializer автоматически генерирует поля из модели — это экономит много времени.

Создание ViewSet

ViewSet объединяет все CRUD-операции в одном классе.

books/views.py:

from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from .models import Author, Book, Review
from .serializers import (
    AuthorSerializer,
    BookListSerializer,
    BookDetailSerializer,
    ReviewSerializer
)

class AuthorViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    filter_backends = [filters.SearchFilter, filters.OrderingFilter]
    search_fields = ['name', 'bio']
    ordering_fields = ['name', 'created_at']

    @action(detail=True, methods=['get'])
    def books(self, request, pk=None):
        author = self.get_object()
        books = author.books.all()
        serializer = BookListSerializer(books, many=True)
        return Response(serializer.data)

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.select_related('author').prefetch_related('reviews')
    permission_classes = [IsAuthenticatedOrReadOnly]
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter
    ]
    filterset_fields = ['author', 'in_stock']
    search_fields = ['title', 'description', 'isbn']
    ordering_fields = ['title', 'price', 'published_date']

    def get_serializer_class(self):
        if self.action == 'list':
            return BookListSerializer
        return BookDetailSerializer

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)

    @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
    def add_review(self, request, pk=None):
        book = self.get_object()

        if Review.objects.filter(book=book, user=request.user).exists():
            return Response(
                {'error': 'You have already reviewed this book'},
                status=status.HTTP_400_BAD_REQUEST
            )

        serializer = ReviewSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(book=book, user=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Совет: ModelViewSet автоматически предоставляет listcreateretrieveupdatepartial_updatedestroy.

Маршрутизация

books/urls.py:

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r'authors', views.AuthorViewSet)
router.register(r'books', views.BookViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

bookstore/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('books.urls')),
]

Совет: DefaultRouter автоматически создаёт все нужные маршруты для ViewSet`а.

Запуск и тестирование

Запуск сервера:

python manage.py runserver

Тестирование через Browsable API:

Открой в браузере: http://127.0.0.1:8000/api/

Ты увидишь интерактивную документацию, где можно тестировать API прямо из браузера.

Тестирование через curl:

# GET — список книг
curl -H 'Accept: application/json' http://127.0.0.1:8000/api/books/

# POST — создание книги
curl -X POST -d '{"title":"New Book","price":29.99}' -H 'Content-Type: application/json' http://127.0.0.1:8000/api/books/

Полный код

bookstore/settings.py (добавления):

INSTALLED_APPS = [
    # ...
    'rest_framework',
    'books',
]

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
}

Структура проекта:

bookstore/
├── manage.py
├── bookstore/
│   ├── settings.py
│   └── urls.py
└── books/
    ├── models.py
    ├── serializers.py
    ├── views.py
    ├── urls.py
    └── admin.py

Задачи для закрепления

Задача 1. Создай модель Category и свяжи её с Book.

Задача 2. Добавь фильтрацию книг по категории.

Задача 3. Создай кастомный эндпоинт /books/search/?q=query.

Задача 4. Настрой пагинацию (10 книг на страницу).

Задача 5. Добавь автодокументацию для всех эндпоинтов.

Ответы:

Задача 1.

class Category(models.Model):
    name = models.CharField(max_length=100)

# В Book добавляем:
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)

Задача 2.

filterset_fields = ['category', 'in_stock']

Задача 3.

@action(detail=False, methods=['get'])
def search(self, request):
    q = request.query_params.get('q', '')
    books = self.get_queryset().filter(title__icontains=q)
    serializer = self.get_serializer(books, many=True)
    return Response(serializer.data)

Задача 4.

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}

Задача 5.

# DRF автоматически создаёт документацию по /api/
# Для кастомных эндпоинтов добавляем description:
@action(detail=False, methods=['get'], description="Поиск книг по названию")

Нюансы и подводные камни

  • ModelSerializer автоматически создаёт поля из модели.
  • ModelViewSet предоставляет все CRUD-операции.
  • DefaultRouter автоматически создаёт маршруты для ViewSet.
  • @action — для кастомных эндпоинтов внутри ViewSet.
  • Browsable API доступен в браузере по адресу /api/.
  • permission_classes контролируют доступ.
  • filter_backends — для фильтрации, поиска, сортировки.
  • select_related и prefetch_related оптимизируют запросы к БД.

Частые ошибки и как их избежать

Ошибка 1: Забыл добавить приложение в INSTALLED_APPS

Добавь 'rest_framework' и своё приложение.

Ошибка 2: Неправильная настройка URL

Используй DefaultRouter или SimpleRouter.

Ошибка 3: Неоптимизированные запросы

Используй select_related и prefetch_related для внешних ключей.

Шпаргалка

КомпонентНазначение
ModelSerializerПреобразует модель в JSON и обратно
ModelViewSetПредоставляет все CRUD-операции
DefaultRouterАвтоматическая маршрутизация
@actionКастомные эндпоинты во ViewSet`е |
Browsable APIИнтерактивная документация

Заключение

Сегодня мы:

  • Установили и настроили Django REST Framework
  • Создали модели, сериализаторы и ViewSet
  • Настроили маршрутизацию
  • Протестировали через Browsable API

КВИЗ

Что дальше?

Оцените статью
IMI-DS - PYTHON LERNEN
Содержание
Оглавление ×