Лекция 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/usersGET /api/v1/users/123GET /api/v1/books?author=ТолстойPOST /api/v1/ordersPUT /api/v1/users/123DELETE /api/v1/books/456❌ Плохо:
GET /api/v1/getUser?id=123POST /api/v1/createUserGET /api/v1/users/123/deletePOST /api/v1/user_123_updateПринципы дизайна URI
-
Используйте существительные, а не глаголы
- ✅
/users,/books - ❌
/getUsers,/createBook
- ✅
-
Используйте множественное число для коллекций
- ✅
/users,/books - ❌
/user,/book
- ✅
-
Используйте иерархию для связанных ресурсов
- ✅
/users/123/orders - ❌
/userOrders?userId=123
- ✅
-
Используйте тире (-) для разделения слов
- ✅
/api/v1/user-profiles - ❌
/api/v1/user_profilesили/api/v1/userProfiles
- ✅
-
Избегайте расширений файлов
- ✅
/api/v1/users - ❌
/api/v1/users.json
- ✅
-
Используйте версионирование
- ✅
/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)
| Код | Название | Описание | Использование |
|---|---|---|---|
| 200 | OK | Запрос успешен | GET, PUT, PATCH |
| 201 | Created | Ресурс создан | POST |
| 202 | Accepted | Запрос принят к обработке | Асинхронные операции |
| 204 | No Content | Нет содержимого для возврата | DELETE, PUT |
Коды перенаправления (3xx)
| Код | Название | Описание | Использование |
|---|---|---|---|
| 301 | Moved Permanently | Ресурс перемещен навсегда | Изменение URI |
| 302 | Found | Временное перенаправление | Временные изменения |
| 304 | Not Modified | Ресурс не изменен | Кэширование |
Коды ошибок клиента (4xx)
| Код | Название | Описание | Использование |
|---|---|---|---|
| 400 | Bad Request | Неверный запрос | Некорректные данные |
| 401 | Unauthorized | Не авторизован | Требуется аутентификация |
| 403 | Forbidden | Доступ запрещен | Нет прав доступа |
| 404 | Not Found | Ресурс не найден | Несуществующий URI |
| 409 | Conflict | Конфликт | Нарушение ограничений |
| 422 | Unprocessable Entity | Необрабатываемая сущность | Ошибки валидации |
| 429 | Too Many Requests | Слишком много запросов | Rate limiting |
Коды ошибок сервера (5xx)
| Код | Название | Описание | Использование |
|---|---|---|---|
| 500 | Internal Server Error | Внутренняя ошибка сервера | Неожиданная ошибка |
| 502 | Bad Gateway | Ошибка шлюза | Проблема с прокси |
| 503 | Service Unavailable | Сервис недоступен | Временная недоступность |
| 504 | Gateway Timeout | Таймаут шлюза | Превышено время ожидания |
6. Формат данных
JSON (JavaScript Object Notation)
JSON — наиболее распространенный формат обмена данными в REST API.
Преимущества:
- Читаемый человеком формат
- Легко парсится
- Поддерживается всеми языками программирования
- Компактный по размеру
Пример запроса:
POST /api/v1/usersContent-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=descGET /api/v1/books?sort=price,-year # по цене asc, по году descПоиск
GET /api/v1/books?q=война+мирGET /api/v1/books?search=война&search_fields=title,author8. Версионирование API
Зачем нужно версионирование
Версионирование позволяет:
- Вносить изменения в API без поломки существующих клиентов
- Поддерживать несколько версий одновременно
- Постепенно мигрировать клиентов на новую версию
Стратегии версионирования
1. URI версионирование
/api/v1/users/api/v2/usersПреимущества:
- Простота реализации
- Ясность для клиентов
Недостатки:
- Нарушает принцип REST (ресурс не меняется)
- Дублирование кода
2. Заголовок версионирования
GET /api/usersAccept: 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/booksX-API-Key: your-api-key-hereПрименение: Простые сервисы, публичные API
2. Bearer Token (JWT)
GET /api/v1/booksAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Применение: Современные веб и мобильные приложения
3. OAuth 2.0
GET /api/v1/booksAuthorization: Bearer access_tokenПрименение: Интеграция с внешними сервисами
4. Basic Authentication
GET /api/v1/booksAuthorization: Basic base64(username:password)Применение: Простые внутренние сервисы (не рекомендуется для production)
Пример реализации JWT
from fastapi import FastAPI, Depends, HTTPException, statusfrom fastapi.security import HTTPBearer, HTTPAuthorizationCredentialsimport 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, statusfrom 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, HTTPExceptionfrom slowapi import Limiter, _rate_limit_exceeded_handlerfrom slowapi.util import get_remote_addressfrom slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)app = FastAPI()app.state.limiter = limiterapp.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 OKX-RateLimit-Limit: 100X-RateLimit-Remaining: 99X-RateLimit-Reset: 164223360012. Документация 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, Queryfrom 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 == 2003. 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 == 40414. Лучшие практики REST API
✅ Рекомендации
-
Используйте правильные HTTP методы
- GET для чтения
- POST для создания
- PUT для полной замены
- PATCH для частичного обновления
- DELETE для удаления
-
Используйте правильные HTTP коды
- 200 для успешного GET/PUT/PATCH
- 201 для успешного создания
- 204 для успешного удаления
- 400 для ошибок клиента
- 404 для несуществующих ресурсов
- 500 для ошибок сервера
-
Версионируйте API
- Используйте
/api/v1/,/api/v2/в URL
- Используйте
-
Используйте пагинацию
- Для больших списков всегда используйте пагинацию
-
Стандартизируйте ответы
- Единый формат для успешных ответов
- Единый формат для ошибок
-
Документируйте API
- Используйте OpenAPI/Swagger
- Добавляйте примеры и описания
-
Обрабатывайте ошибки корректно
- Возвращайте понятные сообщения об ошибках
- Используйте правильные коды состояния
-
Используйте HTTPS
- Всегда используйте HTTPS в production
-
Реализуйте Rate Limiting
- Защищайте API от злоупотреблений
-
Используйте кэширование
- Добавляйте заголовки Cache-Control
- Используйте ETag для условных запросов
❌ Типичные ошибки
-
Использование глаголов в URI
❌ POST /api/createUser✅ POST /api/users -
Использование GET для изменения данных
❌ GET /api/users/123/delete✅ DELETE /api/users/123 -
Возврат ошибок с кодом 200
❌ 200 OK {"error": "User not found"}✅ 404 Not Found {"error": "User not found"} -
Отсутствие версионирования
❌ /api/users✅ /api/v1/users -
Неконсистентные ответы
❌ Иногда {"user": {...}}, иногда {"data": {...}}✅ Всегда {"data": {...}}
15. Пример полного REST API
Структура проекта
books-api/├── main.py├── models.py├── schemas.py├── database.py└── requirements.txtmain.py
from fastapi import FastAPI, HTTPException, status, Depends, Queryfrom typing import List, Optionalfrom uuid import UUID, uuid4
from models import Book, Databasefrom 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 важно:
- Следовать принципам REST
- Правильно использовать HTTP методы и коды состояния
- Версионировать API
- Стандартизировать формат ответов и ошибок
- Документировать API
- Обеспечивать безопасность и производительность
Понимание этих принципов позволяет создавать качественные, масштабируемые и удобные в использовании веб-сервисы.