Перейти к содержимому

Лекция 4. REST API и проектирование веб-сервисов


1. Введение в API

Что такое API

API (Application Programming Interface) — интерфейс программирования приложений, набор правил и протоколов для взаимодействия между различными компонентами программного обеспечения.

В контексте веб-приложений API представляет собой набор эндпойнтов (endpoints), через которые клиентские приложения могут запрашивать данные и выполнять операции на сервере.

Основные характеристики:

  • Стандартизированный формат обмена данными (обычно JSON)
  • Протокол HTTP для передачи запросов и ответов
  • Четко определенные контракты (формат запросов и ответов)
  • Независимость от клиентской платформы (работает с любым клиентом)

Примеры использования:

  • Мобильные приложения получают данные с сервера
  • Веб-сайты взаимодействуют с backend-сервисами
  • Микросервисы обмениваются данными друг с другом
  • Интеграция с внешними сервисами (платежи, карты, социальные сети)

2. REST архитектура

Что такое REST

REST (Representational State Transfer) — архитектурный стиль для проектирования распределенных систем, предложенный Роем Филдингом в 2000 году.

REST не является стандартом или протоколом, а представляет собой набор принципов и ограничений, которые делают веб-сервисы более простыми, масштабируемыми и надежными.

Принципы REST

2.1. Клиент-серверная архитектура

  • Четкое разделение между клиентом и сервером
  • Клиент не зависит от сервера и наоборот
  • Они могут развиваться независимо

2.2. Stateless (без состояния)

  • Каждый запрос содержит всю необходимую информацию для его обработки
  • Сервер не хранит состояние клиента между запросами
  • Упрощает масштабирование и улучшает надежность

2.3. Кэшируемость

  • Ответы должны быть помечены как кэшируемые или некэшируемые
  • Клиенты могут кэшировать ответы для улучшения производительности

2.4. Единообразный интерфейс

  • Унифицированный способ взаимодействия с ресурсами
  • Использование стандартных HTTP методов
  • Ресурсы идентифицируются через URI

2.5. Многоуровневая система

  • Система может состоять из нескольких уровней (прокси, балансировщики, кэши)
  • Каждый компонент видит только соседний уровень

2.6. Код по требованию (опционально)

  • Сервер может предоставлять исполняемый код клиенту (например, JavaScript)

3. Ресурсы и URI

Понятие ресурса

Ресурс — любая сущность, которую можно идентифицировать, назвать или адресовать. В REST ресурсы представляют собой объекты или концепции, с которыми работает приложение.

Примеры ресурсов:

  • Пользователь (/users/123)
  • Книга (/books/456)
  • Заказ (/orders/789)
  • Коллекция книг (/books)

Дизайн URI

Правила именования URI

Хорошо:

GET /api/v1/users
GET /api/v1/users/123
GET /api/v1/books?author=Толстой
POST /api/v1/orders
PUT /api/v1/users/123
DELETE /api/v1/books/456

Плохо:

GET /api/v1/getUser?id=123
POST /api/v1/createUser
GET /api/v1/users/123/delete
POST /api/v1/user_123_update

Принципы дизайна URI

  1. Используйте существительные, а не глаголы

    • /users, /books
    • /getUsers, /createBook
  2. Используйте множественное число для коллекций

    • /users, /books
    • /user, /book
  3. Используйте иерархию для связанных ресурсов

    • /users/123/orders
    • /userOrders?userId=123
  4. Используйте тире (-) для разделения слов

    • /api/v1/user-profiles
    • /api/v1/user_profiles или /api/v1/userProfiles
  5. Избегайте расширений файлов

    • /api/v1/users
    • /api/v1/users.json
  6. Используйте версионирование

    • /api/v1/users, /api/v2/users
    • /api/users (без версии)

4. HTTP методы в REST

Основные методы

МетодИдемпотентностьБезопасностьОписаниеПример
GETПолучение ресурсаGET /users/123
POSTСоздание ресурсаPOST /users
PUTПолная замена ресурсаPUT /users/123
PATCHЧастичное обновлениеPATCH /users/123
DELETEУдаление ресурсаDELETE /users/123
HEADЗаголовки ресурсаHEAD /users/123
OPTIONSПоддерживаемые методыOPTIONS /users

Идемпотентность

Идемпотентность — свойство операции, при котором повторное выполнение дает тот же результат, что и первое выполнение.

  • GET — всегда возвращает тот же ресурс
  • PUT — повторная замена дает тот же результат
  • DELETE — повторное удаление не меняет состояние
  • POST — не идемпотентен (может создавать новые ресурсы каждый раз)

Безопасность

Безопасный метод — метод, который не изменяет состояние сервера.

  • GET, HEAD, OPTIONS — безопасные
  • POST, PUT, PATCH, DELETE — небезопасные

5. HTTP коды состояния

Коды успешного выполнения (2xx)

КодНазваниеОписаниеИспользование
200OKЗапрос успешенGET, PUT, PATCH
201CreatedРесурс созданPOST
202AcceptedЗапрос принят к обработкеАсинхронные операции
204No ContentНет содержимого для возвратаDELETE, PUT

Коды перенаправления (3xx)

КодНазваниеОписаниеИспользование
301Moved PermanentlyРесурс перемещен навсегдаИзменение URI
302FoundВременное перенаправлениеВременные изменения
304Not ModifiedРесурс не измененКэширование

Коды ошибок клиента (4xx)

КодНазваниеОписаниеИспользование
400Bad RequestНеверный запросНекорректные данные
401UnauthorizedНе авторизованТребуется аутентификация
403ForbiddenДоступ запрещенНет прав доступа
404Not FoundРесурс не найденНесуществующий URI
409ConflictКонфликтНарушение ограничений
422Unprocessable EntityНеобрабатываемая сущностьОшибки валидации
429Too Many RequestsСлишком много запросовRate limiting

Коды ошибок сервера (5xx)

КодНазваниеОписаниеИспользование
500Internal Server ErrorВнутренняя ошибка сервераНеожиданная ошибка
502Bad GatewayОшибка шлюзаПроблема с прокси
503Service UnavailableСервис недоступенВременная недоступность
504Gateway TimeoutТаймаут шлюзаПревышено время ожидания

6. Формат данных

JSON (JavaScript Object Notation)

JSON — наиболее распространенный формат обмена данными в REST API.

Преимущества:

  • Читаемый человеком формат
  • Легко парсится
  • Поддерживается всеми языками программирования
  • Компактный по размеру

Пример запроса:

POST /api/v1/users
Content-Type: application/json
{
"name": "Иван Иванов",
"email": "ivan@example.com",
"age": 25
}

Пример ответа:

{
"id": 123,
"name": "Иван Иванов",
"email": "ivan@example.com",
"age": 25,
"created_at": "2024-01-15T10:30:00Z"
}

Структура ответов

Успешный ответ с данными

{
"data": {
"id": 123,
"name": "Иван Иванов",
"email": "ivan@example.com"
}
}

Ответ со списком

{
"data": [
{"id": 1, "name": "Иван"},
{"id": 2, "name": "Петр"}
],
"meta": {
"total": 2,
"page": 1,
"per_page": 10
}
}

Ошибка

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Поле email обязательно для заполнения",
"details": {
"field": "email",
"reason": "required"
}
}
}

7. Пагинация и фильтрация

Пагинация

Пагинация — разбиение больших наборов данных на страницы для улучшения производительности и удобства использования.

Offset-based пагинация

GET /api/v1/books?page=1&per_page=20

Ответ:

{
"data": [...],
"pagination": {
"page": 1,
"per_page": 20,
"total": 150,
"total_pages": 8,
"has_next": true,
"has_prev": false
}
}

Cursor-based пагинация

GET /api/v1/books?cursor=eyJpZCI6MTIzfQ&limit=20

Преимущества:

  • Более эффективна для больших наборов данных
  • Избегает проблем с пропущенными/дублированными записями

Фильтрация

GET /api/v1/books?author=Толстой&year=1869&min_price=100&max_price=1000

Сортировка

GET /api/v1/books?sort_by=price&order=desc
GET /api/v1/books?sort=price,-year # по цене asc, по году desc

Поиск

GET /api/v1/books?q=война+мир
GET /api/v1/books?search=война&search_fields=title,author

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

Зачем нужно версионирование

Версионирование позволяет:

  • Вносить изменения в API без поломки существующих клиентов
  • Поддерживать несколько версий одновременно
  • Постепенно мигрировать клиентов на новую версию

Стратегии версионирования

1. URI версионирование

/api/v1/users
/api/v2/users

Преимущества:

  • Простота реализации
  • Ясность для клиентов

Недостатки:

  • Нарушает принцип REST (ресурс не меняется)
  • Дублирование кода

2. Заголовок версионирования

GET /api/users
Accept: application/vnd.api.v1+json

Преимущества:

  • Чистые URI
  • Соответствие REST принципам

Недостатки:

  • Менее очевидно для разработчиков

3. Параметр запроса

GET /api/users?version=1

Не рекомендуется для production API.

Рекомендации

  • Используйте URI версионирование для простоты
  • Указывайте версию в URL: /api/v1/...
  • Поддерживайте минимум 2 последние версии
  • Документируйте изменения между версиями

9. Аутентификация и авторизация

Аутентификация vs Авторизация

  • Аутентификация — процесс подтверждения личности пользователя (кто вы?)
  • Авторизация — процесс определения прав доступа (что вы можете делать?)

Методы аутентификации

1. API Keys

GET /api/v1/books
X-API-Key: your-api-key-here

Применение: Простые сервисы, публичные API

2. Bearer Token (JWT)

GET /api/v1/books
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Применение: Современные веб и мобильные приложения

3. OAuth 2.0

GET /api/v1/books
Authorization: Bearer access_token

Применение: Интеграция с внешними сервисами

4. Basic Authentication

GET /api/v1/books
Authorization: Basic base64(username:password)

Применение: Простые внутренние сервисы (не рекомендуется для production)

Пример реализации JWT

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
app = FastAPI()
security = HTTPBearer()
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
try:
token = credentials.credentials
payload = jwt.decode(token, "secret-key", algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expired"
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
@app.get("/api/v1/protected")
def protected_route(user: dict = Depends(verify_token)):
return {"message": f"Hello, {user['username']}!"}

10. Обработка ошибок

Стандартизация ошибок

Все ошибки должны возвращаться в едином формате:

{
"error": {
"code": "ERROR_CODE",
"message": "Человекочитаемое сообщение",
"details": {
"field": "email",
"reason": "invalid_format"
},
"timestamp": "2024-01-15T10:30:00Z",
"path": "/api/v1/users"
}
}

Примеры обработки ошибок

from fastapi import FastAPI, HTTPException, status
from pydantic import ValidationError
app = FastAPI()
@app.exception_handler(ValidationError)
async def validation_exception_handler(request, exc):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"error": {
"code": "VALIDATION_ERROR",
"message": "Ошибка валидации данных",
"details": exc.errors()
}
}
)
@app.exception_handler(404)
async def not_found_handler(request, exc):
return JSONResponse(
status_code=404,
content={
"error": {
"code": "NOT_FOUND",
"message": "Ресурс не найден",
"path": str(request.url)
}
}
)

11. Rate Limiting

Что такое Rate Limiting

Rate Limiting — ограничение количества запросов от одного клиента за определенный период времени.

Цели:

  • Защита от злоупотреблений
  • Обеспечение справедливого использования ресурсов
  • Предотвращение DDoS атак

Реализация

from fastapi import FastAPI, Request, HTTPException
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.get("/api/v1/books")
@limiter.limit("10/minute")
async def list_books(request: Request):
return {"books": [...]}

Заголовки Rate Limiting

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1642233600

12. Документация API

OpenAPI/Swagger

OpenAPI (ранее Swagger) — стандарт для описания REST API.

FastAPI автоматически генерирует документацию:

from fastapi import FastAPI
app = FastAPI(
title="Books API",
description="API для управления книгами",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)

Доступ:

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc

Примеры в документации

from fastapi import FastAPI, Path, Query
from pydantic import BaseModel
class BookCreate(BaseModel):
title: str = Field(..., description="Название книги", example="Война и мир")
author: str = Field(..., description="Автор", example="Л.Н. Толстой")
year: int = Field(..., description="Год издания", example=1869)
@app.post(
"/api/v1/books",
response_model=Book,
status_code=status.HTTP_201_CREATED,
summary="Создать книгу",
description="Создает новую книгу в системе",
response_description="Созданная книга"
)
async def create_book(book: BookCreate):
...

13. Тестирование API

Типы тестирования

1. Unit тесты

from fastapi.testclient import TestClient
client = TestClient(app)
def test_create_book():
response = client.post(
"/api/v1/books",
json={"title": "Test", "author": "Author", "year": 2024}
)
assert response.status_code == 201
assert response.json()["title"] == "Test"

2. Integration тесты

def test_get_book_by_id():
# Создаем книгу
create_response = client.post("/api/v1/books", json={...})
book_id = create_response.json()["id"]
# Получаем книгу
get_response = client.get(f"/api/v1/books/{book_id}")
assert get_response.status_code == 200

3. E2E тесты

def test_full_workflow():
# Создание
create_response = client.post("/api/v1/books", json={...})
# Обновление
update_response = client.patch(...)
# Удаление
delete_response = client.delete(...)
# Проверка удаления
get_response = client.get(...)
assert get_response.status_code == 404

14. Лучшие практики REST API

✅ Рекомендации

  1. Используйте правильные HTTP методы

    • GET для чтения
    • POST для создания
    • PUT для полной замены
    • PATCH для частичного обновления
    • DELETE для удаления
  2. Используйте правильные HTTP коды

    • 200 для успешного GET/PUT/PATCH
    • 201 для успешного создания
    • 204 для успешного удаления
    • 400 для ошибок клиента
    • 404 для несуществующих ресурсов
    • 500 для ошибок сервера
  3. Версионируйте API

    • Используйте /api/v1/, /api/v2/ в URL
  4. Используйте пагинацию

    • Для больших списков всегда используйте пагинацию
  5. Стандартизируйте ответы

    • Единый формат для успешных ответов
    • Единый формат для ошибок
  6. Документируйте API

    • Используйте OpenAPI/Swagger
    • Добавляйте примеры и описания
  7. Обрабатывайте ошибки корректно

    • Возвращайте понятные сообщения об ошибках
    • Используйте правильные коды состояния
  8. Используйте HTTPS

    • Всегда используйте HTTPS в production
  9. Реализуйте Rate Limiting

    • Защищайте API от злоупотреблений
  10. Используйте кэширование

    • Добавляйте заголовки Cache-Control
    • Используйте ETag для условных запросов

❌ Типичные ошибки

  1. Использование глаголов в URI

    ❌ POST /api/createUser
    ✅ POST /api/users
  2. Использование GET для изменения данных

    ❌ GET /api/users/123/delete
    ✅ DELETE /api/users/123
  3. Возврат ошибок с кодом 200

    ❌ 200 OK {"error": "User not found"}
    ✅ 404 Not Found {"error": "User not found"}
  4. Отсутствие версионирования

    ❌ /api/users
    ✅ /api/v1/users
  5. Неконсистентные ответы

    ❌ Иногда {"user": {...}}, иногда {"data": {...}}
    ✅ Всегда {"data": {...}}

15. Пример полного REST API

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

books-api/
├── main.py
├── models.py
├── schemas.py
├── database.py
└── requirements.txt

main.py

from fastapi import FastAPI, HTTPException, status, Depends, Query
from typing import List, Optional
from uuid import UUID, uuid4
from models import Book, Database
from schemas import BookCreate, BookUpdate, BookResponse
app = FastAPI(
title="Books API",
description="REST API для управления книгами",
version="1.0.0"
)
db = Database()
# Получить все книги
@app.get("/api/v1/books", response_model=List[BookResponse], tags=["books"])
def list_books(
page: int = Query(1, ge=1),
per_page: int = Query(10, ge=1, le=100),
author: Optional[str] = None,
min_price: Optional[float] = None,
max_price: Optional[float] = None
):
books = db.get_books()
# Фильтрация
if author:
books = [b for b in books if b.author == author]
if min_price is not None:
books = [b for b in books if b.price >= min_price]
if max_price is not None:
books = [b for b in books if b.price <= max_price]
# Пагинация
start = (page - 1) * per_page
end = start + per_page
paginated_books = books[start:end]
return paginated_books
# Получить книгу по ID
@app.get("/api/v1/books/{book_id}", response_model=BookResponse, tags=["books"])
def get_book(book_id: UUID):
book = db.get_book(book_id)
if not book:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found"
)
return book
# Создать книгу
@app.post(
"/api/v1/books",
response_model=BookResponse,
status_code=status.HTTP_201_CREATED,
tags=["books"]
)
def create_book(book_data: BookCreate):
book = db.create_book(book_data)
return book
# Полная замена книги
@app.put("/api/v1/books/{book_id}", response_model=BookResponse, tags=["books"])
def replace_book(book_id: UUID, book_data: BookCreate):
book = db.update_book(book_id, book_data)
if not book:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found"
)
return book
# Частичное обновление
@app.patch("/api/v1/books/{book_id}", response_model=BookResponse, tags=["books"])
def update_book(book_id: UUID, book_data: BookUpdate):
book = db.partial_update_book(book_id, book_data)
if not book:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found"
)
return book
# Удалить книгу
@app.delete(
"/api/v1/books/{book_id}",
status_code=status.HTTP_204_NO_CONTENT,
tags=["books"]
)
def delete_book(book_id: UUID):
success = db.delete_book(book_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Book not found"
)
return None

Заключение

REST API — это стандартный способ создания веб-сервисов, который обеспечивает:

  • Простоту использования — понятные правила и стандарты
  • Масштабируемость — stateless архитектура
  • Независимость — клиент и сервер могут развиваться отдельно
  • Кэшируемость — улучшение производительности
  • Стандартизацию — использование HTTP протокола

При проектировании REST API важно:

  1. Следовать принципам REST
  2. Правильно использовать HTTP методы и коды состояния
  3. Версионировать API
  4. Стандартизировать формат ответов и ошибок
  5. Документировать API
  6. Обеспечивать безопасность и производительность

Понимание этих принципов позволяет создавать качественные, масштабируемые и удобные в использовании веб-сервисы.