Что такое RESTful API

REST - это теория. RESTful - это когда теория работает в продакшене

Что такое RESTful API.md

Содержание


Коротко и чётко

RESTful API - это API, спроектированное по принципам REST. Четыре практических правила:

  • Ресурсы в URL - адрес описывает существительное, а не действие (/users, не /getUsers)
  • HTTP-методы по назначению - GET читает, POST создаёт, PUT/PATCH обновляет, DELETE удаляет
  • Статус-коды по смыслу - 200 успех, 201 создано, 404 не найдено, 400 ошибка клиента, 500 ошибка сервера
  • JSON как формат - данные передаются в JSON, тип указывается в заголовке Content-Type: application/json

RESTful API - это конкретная реализация REST-принципов. На практике это значит: URL описывает ресурс через существительное, HTTP-метод описывает действие, ответ возвращается с правильным статус-кодом, данные в JSON. Один и тот же URL /users/1 при GET вернёт пользователя, при PUT обновит, при DELETE удалит. Это и есть единый интерфейс из принципов REST на практике.


Простыми словами

Представь библиотеку. В ней есть книги - это ресурсы. У каждой книги есть адрес на полке - это URL. А действия с книгой стандартные для любой библиотеки мира: взять почитать, вернуть, добавить новую, списать старую.

RESTful API устроено так же. Есть ресурсы - пользователи, статьи, заказы. У каждого есть адрес. А действия всегда одни и те же - получить, создать, обновить, удалить.

Любой разработчик, впервые увидевший твоё RESTful API, сразу поймёт как с ним работать - потому что правила везде одинаковые.


Главное правило URL - существительное, не глагол

URL описывает ресурс. Действие описывает HTTP-метод. Смешивать их - ошибка.

# ПЛОХО - глаголы в URL
/getUser/1
/createUser
/deleteUser/1
/getUserOrders/1

# ХОРОШО - только существительные
/users/1
/users
/users/1
/users/1/orders

HTTP-методы - каждый на своём месте

Один URL - разные действия в зависимости от метода:

GET    /users        - получить список всех пользователей
POST   /users        - создать нового пользователя

GET    /users/1      - получить пользователя с id=1
PUT    /users/1      - полностью обновить пользователя с id=1
PATCH  /users/1      - частично обновить пользователя с id=1
DELETE /users/1      - удалить пользователя с id=1

Статус-коды - ответ должен говорить сам за себя

Клиент не должен парсить текст ответа, чтобы понять - успех это или ошибка. Для этого есть статус-коды:

200 OK                  - запрос выполнен успешно
201 Created             - ресурс создан (ответ на POST)
204 No Content          - успешно, но возвращать нечего (ответ на DELETE)

400 Bad Request         - клиент прислал что-то не то
401 Unauthorized        - нужна авторизация
403 Forbidden           - авторизован, но доступа нет
404 Not Found           - ресурс не найден
422 Unprocessable Entity - данные понятны, но невалидны

500 Internal Server Error - что-то сломалось на сервере

Код

Правильная структура URL

# urls.py - как выглядит RESTful роутинг в Django

from django.urls import path
from . import views

urlpatterns = [
    # Коллекция ресурсов
    path('api/v1/users/',          views.UserListCreateView.as_view()),

    # Конкретный ресурс
    path('api/v1/users/<int:pk>/', views.UserDetailView.as_view()),

    # Вложенный ресурс - заказы конкретного пользователя
    path('api/v1/users/<int:pk>/orders/', views.UserOrdersView.as_view()),
]

# Что происходит при разных методах на одном URL:
# GET    /api/v1/users/       -> UserListCreateView.get()    - список
# POST   /api/v1/users/       -> UserListCreateView.post()   - создать
# GET    /api/v1/users/1/     -> UserDetailView.get()        - получить
# PUT    /api/v1/users/1/     -> UserDetailView.put()        - обновить
# DELETE /api/v1/users/1/     -> UserDetailView.delete()     - удалить

Правильные статус-коды в ответах

# views.py
from django.http import JsonResponse
from django.views import View
from .models import Article
import json


class ArticleListCreateView(View):

    def get(self, request):
        articles = list(Article.objects.values('id', 'title', 'content'))
        return JsonResponse(articles, safe=False, status=200)  # 200 OK

    def post(self, request):
        try:
            data = json.loads(request.body)
        except json.JSONDecodeError:
            return JsonResponse(
                {'error': 'Невалидный JSON'},
                status=400  # 400 Bad Request - клиент прислал мусор
            )

        if not data.get('title'):
            return JsonResponse(
                {'error': 'Поле title обязательно'},
                status=422  # 422 - данные поняли, но они невалидны
            )

        article = Article.objects.create(
            title=data['title'],
            content=data.get('content', '')
        )
        return JsonResponse(
            {'id': article.id, 'title': article.title},
            status=201  # 201 Created - ресурс создан
        )


class ArticleDetailView(View):

    def get_object(self, pk):
        try:
            return Article.objects.get(pk=pk)
        except Article.DoesNotExist:
            return None

    def get(self, request, pk):
        article = self.get_object(pk)
        if not article:
            return JsonResponse(
                {'error': 'Статья не найдена'},
                status=404  # 404 Not Found
            )
        return JsonResponse(
            {'id': article.id, 'title': article.title, 'content': article.content},
            status=200
        )

    def patch(self, request, pk):
        article = self.get_object(pk)
        if not article:
            return JsonResponse({'error': 'Статья не найдена'}, status=404)

        data = json.loads(request.body)
        # PATCH - обновляем только то, что пришло
        if 'title' in data:
            article.title = data['title']
        if 'content' in data:
            article.content = data['content']
        article.save()

        return JsonResponse(
            {'id': article.id, 'title': article.title},
            status=200
        )

    def delete(self, request, pk):
        article = self.get_object(pk)
        if not article:
            return JsonResponse({'error': 'Статья не найдена'}, status=404)

        article.delete()
        return JsonResponse({}, status=204)  # 204 No Content - удалено, тела нет

То же самое на Django REST Framework - короче и чище

# serializers.py
from rest_framework import serializers
from .models import Article

class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ['id', 'title', 'content', 'created_at']
        read_only_fields = ['id', 'created_at']
# views.py
from rest_framework import generics, status
from rest_framework.response import Response
from .models import Article
from .serializers import ArticleSerializer


class ArticleListCreateView(generics.ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    # GET  -> 200 + список
    # POST -> 201 + созданный объект
    # Всё остальное DRF делает сам


class ArticleDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    # GET    -> 200 + объект
    # PUT    -> 200 + обновлённый объект
    # PATCH  -> 200 + частично обновлённый объект
    # DELETE -> 204 No Content
    # 404 при отсутствии - автоматически
# urls.py
from django.urls import path
from .views import ArticleListCreateView, ArticleDetailView

urlpatterns = [
    path('api/v1/articles/',          ArticleListCreateView.as_view()),
    path('api/v1/articles/<int:pk>/', ArticleDetailView.as_view()),
]

Версионирование API

# Версия в URL - самый распространённый подход
urlpatterns = [
    path('api/v1/articles/', ArticleListCreateViewV1.as_view()),
    path('api/v2/articles/', ArticleListCreateViewV2.as_view()),
]

# v1 живёт, пока есть клиенты которые его используют
# v2 вводит новые поля или меняет структуру
# Клиенты мигрируют постепенно

Глубже

PUT против PATCH - частая путаница. PUT заменяет ресурс целиком. Если у пользователя есть поля name, email, phone - и ты делаешь PUT передавая только name, поля email и phone обнулятся. PATCH обновляет только то, что пришло. На практике PATCH используют чаще - он безопаснее. PUT имеет смысл когда клиент владеет полной моделью и намеренно заменяет её целиком.

Идемпотентность. GET, PUT, DELETE - идемпотентные методы. Вызови их десять раз с одними параметрами - результат тот же, что и при первом вызове. POST - не идемпотентен, каждый вызов создаёт новый ресурс. PATCH формально не идемпотентен, хотя на практике чаще всего ведёт себя идемпотентно. Это важно при обработке сетевых ошибок: идемпотентный запрос можно безопасно повторить, если ответ не пришёл.

Вложенные ресурсы - до двух уровней. Структуру /users/1/orders/5/items/3 поддерживать болезненно. Принятое правило - не глубже двух уровней вложенности. Если нужно глубже - лучше использовать фильтрацию: /items?order_id=5&user_id=1.

Пагинация - обязательна для списков. Возвращать все записи таблицы одним запросом - плохая идея. Стандартные подходы:

# Offset-based - простой, но медленный на больших данных
GET /api/v1/articles/?page=2&page_size=20

# Cursor-based - быстрый, подходит для бесконечного скролла
GET /api/v1/articles/?cursor=eyJpZCI6IDEwMH0&page_size=20

Фильтрация, сортировка, поиск - через query параметры:

GET /api/v1/articles/?status=published          - фильтрация
GET /api/v1/articles/?ordering=-created_at      - сортировка (- означает DESC)
GET /api/v1/articles/?search=python             - поиск
GET /api/v1/articles/?status=published&ordering=-created_at&page=1  - всё вместе

FAQ

Чем PUT отличается от PATCH? `PUT` заменяет ресурс целиком - нужно передать все поля. Если поле не передать, оно обнулится или вызовет ошибку. `PATCH` обновляет частично - передаёшь только изменившиеся поля. На практике `PATCH` удобнее и безопаснее, его используют чаще.
Почему DELETE возвращает 204, а не 200? `204 No Content` означает «успешно выполнено, но тело ответа пустое». После удаления возвращать нечего - ресурса больше нет. Технически можно вернуть `200` с пустым телом, но `204` семантически точнее и это принятое соглашение.
Нужно ли всегда версионировать API? Для публичного API - да, с первой версии. Ты не знаешь, кто и как использует твоё API, и не можешь сломать существующих клиентов. Для внутреннего API между своими сервисами можно договориться о миграции и обойтись без версионирования. Но завести `/v1/` сразу - дешевле, чем разгребать последствия потом.
Что возвращать в теле ответа при ошибке? Единого стандарта нет, но принятая практика - JSON с полем `error` или `detail` и понятным сообщением. DRF по умолчанию возвращает `{"detail": "Not found."}`. Хорошая идея добавить `code` - машиночитаемый код ошибки, по которому клиент может принять решение не парся текст.
{
  "error": "Пользователь не найден",
  "code": "user_not_found"
}
Как передавать авторизацию в RESTful API? Через заголовок `Authorization`. Самый распространённый вариант сейчас - Bearer-токен: `Authorization: Bearer `. Токен хранится у клиента и передаётся в каждом запросе - это соответствует принципу stateless. Куки с серверной сессией технически нарушают stateless, хотя на практике многие так и делают.
Чем RESTful отличается от REST? REST - это архитектурный стиль, теория, набор принципов. RESTful - прилагательное, описывающее конкретное API, которое эти принципы соблюдает. Правильно говорить «RESTful API». «REST API» - разговорный вариант, все понимают о чём речь, но строго говоря это не совсем точно.