Что такое RESTful API
REST - это теория. RESTful - это когда теория работает в продакшене
Содержание
Коротко и чётко
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"
}