Лекция 5. REST API: проектирование веб-сервисов
Раздел 2. Разработка интернет-приложений (backend, Python/FastAPI) Длительность пары: ~1 ч 30 мин
План лекции
- Что такое API
- Архитектурный стиль REST и его принципы
- Ресурсы и проектирование URI
- HTTP-методы и сопоставление с CRUD
- Коды состояния HTTP
- Формат данных и согласование контента
1. Что такое API
API (Application Programming Interface) — интерфейс программирования приложений, набор правил и соглашений, по которым одни программные компоненты обращаются к другим.
В контексте веб-приложений API — это набор эндпойнтов (точек входа), через которые клиент запрашивает данные и выполняет операции на сервере. Клиенту не нужно знать, как сервер устроен внутри: он работает только с описанным «контрактом».
Ключевые характеристики веб-API:
- Стандартизированный формат обмена данными (чаще всего JSON);
- Протокол HTTP для передачи запросов и ответов;
- Чётко определённый контракт — формат запросов и ответов известен заранее;
- Независимость от платформы клиента — API одинаково работает с веб-сайтом, мобильным приложением или другим сервисом.
Где применяется:
- мобильные приложения получают данные с сервера;
- веб-фронтенд (SPA) обращается к backend-сервисам;
- микросервисы обмениваются данными друг с другом;
- интеграция с внешними системами (платежи, карты, соцсети).
Аналогия: API — это «меню в ресторане». Вы выбираете блюдо по названию и не видите, что происходит на кухне. Меню — контракт, кухня — реализация.
2. Архитектурный стиль REST
REST (Representational State Transfer) — архитектурный стиль для проектирования распределённых систем, предложенный Роем Филдингом в 2000 году в его диссертации.
Важно понимать: REST — это не протокол и не стандарт, а набор ограничений (constraints). API, который им соответствует, называют RESTful. Соблюдение этих ограничений делает сервис простым, масштабируемым и предсказуемым.
Шесть принципов REST
2.1. Клиент-серверная архитектура
Чёткое разделение ответственности: клиент отвечает за интерфейс пользователя, сервер — за хранение и обработку данных. Они слабо связаны и могут развиваться независимо друг от друга.
2.2. Stateless (отсутствие состояния)
Каждый запрос содержит всю информацию, необходимую для его обработки (например, токен авторизации). Сервер не хранит состояние клиента между запросами. Это упрощает горизонтальное масштабирование: любой запрос может обработать любой экземпляр сервера.
2.3. Кешируемость
Ответы должны явно указывать, можно ли их кешировать (заголовки Cache-Control, ETag). Кеширование снижает нагрузку на сервер и ускоряет отклик для клиента.
2.4. Единообразие интерфейса (uniform interface)
Главный принцип REST. Взаимодействие со всеми ресурсами происходит единообразно:
- ресурсы идентифицируются через URI;
- операции выполняются стандартными HTTP-методами;
- ответ содержит всё необходимое для дальнейшей работы с ресурсом.
2.5. Слоистая система (layered system)
Между клиентом и сервером могут стоять промежуточные слои — прокси, балансировщики нагрузки, кеши. Каждый компонент «видит» только соседний слой и не знает о всей цепочке.
2.6. Код по требованию (опционально)
Сервер может передавать клиенту исполняемый код (например, JavaScript). Единственный необязательный принцип.
3. Ресурсы и проектирование URI
Понятие ресурса
Ресурс — любая сущность, которую можно идентифицировать и адресовать: пользователь, книга, заказ, коллекция книг. В REST вся работа строится вокруг ресурсов, а URI — это их адрес.
/users/123 — конкретный пользователь/users — коллекция пользователей/books/456 — конкретная книга/users/123/orders — заказы пользователя 123Правила проектирования URI
| Правило | Хорошо | Плохо |
|---|---|---|
| Существительные, а не глаголы | GET /users | GET /getUsers |
| Множественное число для коллекций | /books | /book |
| Иерархия для связанных ресурсов | /users/123/orders | /userOrders?userId=123 |
| Тире для разделения слов | /user-profiles | /user_profiles, /userProfiles |
| Без расширений файлов | /users | /users.json |
| Версионирование в пути | /api/v1/users | /api/users |
Ключевая идея: действие выражается HTTP-методом, а не словом в URI. Глагол delete не нужен — для удаления есть метод DELETE.
✅ DELETE /api/v1/books/456❌ GET /api/v1/books/456/deleteПараметры запроса (query string) применяют для фильтрации, сортировки и пагинации, но не для идентификации ресурса:
GET /api/v1/books?author=Толстой&sort=year&page=24. HTTP-методы и CRUD
REST опирается на стандартные HTTP-методы. Каждый метод соответствует операции над ресурсом. Базовый набор операций называют CRUD (Create, Read, Update, Delete).
Сопоставление: метод → операция → код успеха
| Метод | CRUD-операция | Над чем | Идемпотентность | Типичный код успеха |
|---|---|---|---|---|
| GET | Read | ресурс / коллекция | да | 200 OK |
| POST | Create | коллекция | нет | 201 Created |
| PUT | Update (полная замена) | ресурс | да | 200 OK / 204 No Content |
| PATCH | Update (частично) | ресурс | нет | 200 OK |
| DELETE | Delete | ресурс | да | 204 No Content |
Идемпотентность и безопасность
- Идемпотентность — повторное выполнение запроса даёт тот же результат.
GET,PUT,DELETEидемпотентны;POSTиPATCH— нет. - Безопасный метод — не изменяет состояние сервера. Безопасны только
GET,HEAD,OPTIONS.
Примеры на FastAPI
from fastapi import FastAPI, HTTPException, statusfrom pydantic import BaseModel
app = FastAPI(title="Books API", version="1.0.0")
class BookCreate(BaseModel): title: str author: str year: int
class Book(BookCreate): id: int
books: dict[int, Book] = {}counter = 0
# READ — список@app.get("/api/v1/books", response_model=list[Book])def list_books(): return list(books.values())
# READ — один ресурс@app.get("/api/v1/books/{book_id}", response_model=Book)def get_book(book_id: int): book = books.get(book_id) if book is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found") return book
# CREATE@app.post("/api/v1/books", response_model=Book, status_code=status.HTTP_201_CREATED)def create_book(data: BookCreate): global counter counter += 1 book = Book(id=counter, **data.model_dump()) books[counter] = book return book
# UPDATE — полная замена@app.put("/api/v1/books/{book_id}", response_model=Book)def replace_book(book_id: int, data: BookCreate): if book_id not in books: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found") book = Book(id=book_id, **data.model_dump()) books[book_id] = book return book
# DELETE@app.delete("/api/v1/books/{book_id}", status_code=status.HTTP_204_NO_CONTENT)def delete_book(book_id: int): if books.pop(book_id, None) is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found") return NonePUT и PATCH отличаются объёмом данных: PUT заменяет ресурс целиком (все поля обязательны), PATCH обновляет только переданные поля. Для PATCH обычно делают схему с необязательными полями:
class BookUpdate(BaseModel): title: str | None = None author: str | None = None year: int | None = None
@app.patch("/api/v1/books/{book_id}", response_model=Book)def update_book(book_id: int, data: BookUpdate): book = books.get(book_id) if book is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found") updated = book.model_copy(update=data.model_dump(exclude_unset=True)) books[book_id] = updated return updated5. Коды состояния HTTP
Код состояния — это короткий сигнал клиенту о результате запроса. Правильно выбранный код важен не меньше, чем тело ответа.
Группы кодов
| Группа | Значение |
|---|---|
| 2xx | Успех |
| 3xx | Перенаправление |
| 4xx | Ошибка на стороне клиента |
| 5xx | Ошибка на стороне сервера |
Наиболее частые коды
| Код | Название | Когда использовать |
|---|---|---|
| 200 | OK | Успешные GET, PUT, PATCH |
| 201 | Created | Ресурс создан (POST) |
| 204 | No Content | Успех без тела ответа (DELETE) |
| 400 | Bad Request | Некорректный запрос (например, битый JSON) |
| 401 | Unauthorized | Требуется аутентификация |
| 403 | Forbidden | Аутентифицирован, но нет прав |
| 404 | Not Found | Ресурс не найден |
| 409 | Conflict | Конфликт (например, дубликат) |
| 422 | Unprocessable Entity | Ошибка валидации данных |
| 500 | Internal Server Error | Непредвиденная ошибка сервера |
Различайте 401 и 403: 401 — «я не знаю, кто вы» (нет/неверный токен); 403 — «я знаю, кто вы, но вам сюда нельзя».
Частая ошибка: возвращать 200 OK с телом {"error": "..."}. Так делать нельзя — код должен отражать результат. Ошибку «не найдено» сопровождают кодом 404, а не 200.
В FastAPI код задаётся параметром status_code в декораторе или передаётся в HTTPException. Валидацию входных данных по Pydantic-схеме FastAPI выполняет автоматически и при ошибке отдаёт 422.
6. Формат данных и согласование контента
JSON
JSON (JavaScript Object Notation) — самый распространённый формат обмена в 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": "2026-01-15T10:30:00Z"}Согласование контента (content negotiation)
Клиент и сервер договариваются о формате данных через HTTP-заголовки:
Content-Type— формат тела запроса, который отправляет клиент (application/json);Accept— формат, который клиент готов принять в ответе.
Если сервер не умеет отдавать запрошенный в Accept формат, он отвечает 406 Not Acceptable. FastAPI по умолчанию сериализует ответы в JSON и сам проставляет Content-Type: application/json.
Согласованная структура ответов
Полезно придерживаться единого формата ответов во всём API.
Список с метаданными:
{ "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"} }}В FastAPI структуру ответа задают через Pydantic-модели и параметр response_model. Это даёт два эффекта сразу: сервер гарантирует форму ответа и автоматически документирует её в OpenAPI (страница /docs).
class UserResponse(BaseModel): id: int name: str email: str
@app.get("/api/v1/users/{user_id}", response_model=UserResponse)def get_user(user_id: int): ...Краткие итоги
- API — контракт взаимодействия программ; веб-API обычно работает поверх HTTP и обменивается JSON.
- REST — архитектурный стиль (не протокол) с шестью принципами: клиент-сервер, stateless, кешируемость, единообразие интерфейса, слоистость, код по требованию.
- Ресурсы адресуются через URI: существительные, множественное число, иерархия; действие выражается HTTP-методом, а не словом в адресе.
- HTTP-методы сопоставляются с CRUD:
POST→Create,GET→Read,PUT/PATCH→Update,DELETE→Delete. - Коды состояния обязательно отражают результат:
200/201/204— успех,4xx— вина клиента,5xx— вина сервера. - JSON — основной формат; форматы согласуются заголовками
Content-TypeиAccept. - FastAPI упрощает всё перечисленное: маршруты на методах,
status_code, Pydantic-схемы,response_modelи автодокументация OpenAPI.
Вопросы для самопроверки
- Что такое API и какие основные характеристики у веб-API?
- Почему REST называют архитектурным стилем, а не протоколом?
- Перечислите шесть принципов REST. Какой из них необязателен?
- Что означает свойство stateless и как оно помогает масштабированию?
- Сформулируйте основные правила проектирования URI. Почему в URI не должно быть глаголов?
- Сопоставьте HTTP-методы с операциями CRUD.
- Чем отличается
PUTотPATCH? - Что такое идемпотентность? Какие методы идемпотентны, а какие — нет?
- Какие коды состояния стоит вернуть при создании ресурса, при удалении и при отсутствии ресурса?
- В чём разница между кодами 401 и 403? Между 400 и 422?
- За что отвечают заголовки
Content-TypeиAccept? - Как
response_modelв FastAPI связан с форматом ответа и документацией?