Цели модуля
После модуля студент сможет:
- Проектировать REST API по принципам ресурсоориентированности
- Выбирать правильные HTTP методы и коды ответов
- Реализовывать стратегии версионирования API
- Проектировать контракты запросов и ответов
- Использовать RFC 7807 Problem Details для ошибок
- Писать OpenAPI спецификации
1.1 Принципы REST
Что такое REST
REST (Representational State Transfer) - архитектурный стиль для проектирования сетевых приложений. Основные принципы:
- Ресурсоориентированность: всё является ресурсом
- Единообразие интерфейса: стандартные HTTP методы
- Stateless: каждый запрос содержит всю необходимую информацию
- Клиент-сервер: разделение ответственности
- Кэшируемость: ответы могут быть кэшированы
- Слоистая система: промежуточные слои прозрачны
Ресурсы и URL
Ресурс - это сущность которой мы управляем через API.
Правила проектирования URL:
sql# ПРАВИЛЬНО GET /api/v1/repos # список репозиториев POST /api/v1/repos # создать репозиторий GET /api/v1/repos/{id} # получить репозиторий PUT /api/v1/repos/{id} # обновить репозиторий DELETE /api/v1/repos/{id} # удалить репозиторий # Вложенные ресурсы GET /api/v1/repos/{id}/pulls # pull requests репозитория
Правила:
- Существительные во множественном числе: /repos, /users
- Без глаголов в URL: не /getRepos, а GET /repos
- Вложенность максимум 2 уровня: /repos/{id}/pulls
- Дефисы вместо подчёркиваний: /pull-requests
- Строчные буквы: /api/v1/repos
Ресурсы vs Действия
REST моделирует состояние ресурсов. Действия выражаются через HTTP методы, не через URL.
text# ПРАВИЛЬНО: действие через HTTP метод POST /api/v1/repos/{id}/merge # merge - под-ресурс с состоянием # ПЛОХО: глагол в URL POST /api/v1/repos/{id}/doMerge
Исключение: действия не моделируемые как состояние ресурса:
textPOST /api/v1/auth/login POST /api/v1/auth/logout POST /api/v1/payments/{id}/refund
1.2 HTTP методы
Семантика методов
| Метод | Назначение | Идемпотентность | Тело запроса | Тело ответа |
|---|---|---|---|---|
| GET | Получить ресурс | Идемпотентен | Нет | Да |
| POST | Создать ресурс | Не идемпотентен | Да | Да |
| PUT | Заменить ресурс целиком | Идемпотентен | Да | Да |
| PATCH | Частичное обновление | Не идемпотентен | Да | Да |
| DELETE | Удалить ресурс | Идемпотентен | Нет | Опционально |
| HEAD | Метаданные ресурса | Идемпотентен | Нет | Нет |
| OPTIONS | Доступные методы | Идемпотентен | Нет | Да |
Идемпотентность
Идемпотентный запрос: повторный вызов даёт тот же результат.
GET /api/v1/repos/123 - всегда возвращает один ресурс. Идемпотентен. DELETE /api/v1/repos/123 - первый вызов удаляет, повторный вернёт 404. Идемпотентен. POST /api/v1/repos - каждый вызов создаёт новый ресурс. Не идемпотентен.
Безопасность метода
Безопасный метод не изменяет состояние сервера. GET, HEAD, OPTIONS - безопасные. POST, PUT, PATCH, DELETE - небезопасные.
Пример: когда PUT, когда PATCH
PUT - полная замена ресурса:
PUT /api/v1/repos/{id}
{
"name": "new-name",
"description": "new description",
"is_private": true
}
// Все поля заменяются. Не переданные поля сбрасываются.
PATCH - частичное обновление:
PATCH /api/v1/repos/{id}
{
"name": "new-name"
}
// Обновляется только name. Остальные поля не изменяются.
1.3 HTTP коды ответов
Категории кодов
| Диапазон | Категория | Назначение |
|---|---|---|
| 1xx | Informational | Промежуточные ответы |
| 2xx | Success | Успешная обработка |
| 3xx | Redirection | Перенаправление |
| 4xx | Client Error | Ошибка клиента |
| 5xx | Server Error | Ошибка сервера |
Основные коды для API
Успех:
| Код | Когда использовать |
|---|---|
| 200 OK | Успешный GET, PUT, PATCH |
| 201 Created | Успешный POST (ресурс создан) |
| 204 No Content | Успешный DELETE (нет тела ответа) |
Ошибки клиента:
| Код | Когда использовать |
|---|---|
| 400 Bad Request | Невалидный запрос (формат, валидация) |
| 401 Unauthorized | Не аутентифицирован |
| 403 Forbidden | Аутентифицирован, но нет прав |
| 404 Not Found | Ресурс не найден |
| 409 Conflict | Конфликт (дубликат, состояние) |
| 422 Unprocessable Entity | Семантическая ошибка валидации |
| 429 Too Many Requests | Rate limit превышен |
Ошибки сервера:
| Код | Когда использовать |
|---|---|
| 500 Internal Server Error | Непредвиденная ошибка |
| 502 Bad Gateway | Ошибка upstream сервиса |
| 503 Service Unavailable | Сервис временно недоступен |
| 504 Gateway Timeout | Таймаут upstream сервиса |
Правила выбора кодов
- Используйте наиболее специфичный код
- Не возвращайте 200 для ошибок
- 400 для ошибок формата, 422 для ошибок семантики
- 401 когда нет токена, 403 когда токен есть но прав нет
- 500 только для непредвиденных ошибок
Пример из шаблона
gofunc (h *Handler) handleError(w http.ResponseWriter, err error) { switch { case errors.Is(err, service.ErrRepoNotFound): h.writeError(w, http.StatusNotFound, "repo not found") case errors.Is(err, service.ErrRepoAlreadyExists): h.writeError(w, http.StatusConflict, "repo already exists") case errors.Is(err, service.ErrUnauthorized): h.writeError(w, http.StatusUnauthorized, "unauthorized") case errors.Is(err, service.ErrForbidden): h.writeError(w, http.StatusForbidden, "forbidden") default: h.logger.Error("unexpected error", "error", err) h.writeError(w, http.StatusInternalServerError, "internal server error") } }
1.4 Стратегии версионирования API
Зачем нужно версионирование
API эволюционирует. Клиенты зависят от контракта. Breaking changes ломают клиентов. Версионирование позволяет:
- Вносить изменения без поломки существующих клиентов
- Поддерживать несколько версий параллельно
- Давать время на миграцию
Что является breaking change
Breaking changes:
- Удаление поля из ответа
- Изменение типа поля
- Изменение обязательности поля
- Удаление эндпоинта
- Изменение кодов ответа
Non-breaking changes:
- Добавление нового поля в ответ
- Добавление нового эндпоинта
- Добавление optional поля в запрос
- Добавление нового кода ответа
Стратегия 1: URL версионирование
textGET /api/v1/repos GET /api/v2/repos
Плюсы:
- Простота понимания
- Легко тестировать в браузере
- Явное разделение
Минусы:
- URL меняется при смене версии
- Не совсем RESTful (версия не часть ресурса)
Рекомендуется для большинства проектов.
Стратегия 2: Header версионирование
httpGET /api/repos Accept: application/vnd.myapi.v1+json GET /api/repos Accept: application/vnd.myapi.v2+json
Плюсы:
- URL не меняется
- Более RESTful
Минусы:
- Сложнее тестировать
- Менее очевидно для разработчиков
Стратегия 3: Query parameter
textGET /api/repos?version=1
Плюсы:
- Простота
Минусы:
- Не рекомендуется (параметры запроса для данных)
Рекомендация
Используйте URL версионирование (/api/v1/, /api/v2/) для большинства проектов. Это простой, понятный и легко тестируемый подход.
Пример из шаблона
internal/handlers/http/
├── api/
│ └── v1/
│ ├── handler.go
│ ├── dto.go
│ ├── mapper.go
│ └── routes.go
go// Регистрация роутов func RegisterRoutes(mux *http.ServeMux, h *Handler) { mux.HandleFunc("GET /api/v1/repos", h.ListRepos) mux.HandleFunc("POST /api/v1/repos", h.CreateRepo) // ... }
1.5 Контракты запросов и ответов
Что такое контракт API
Контракт API - формальное описание:
- Какие эндпоинты доступны
- Какие параметры принимает каждый эндпоинт
- Какой формат ответа возвращает
- Какие ошибки могут быть
Проектирование запросов
Структура запроса:
POST /api/v1/repos HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer <token>
X-Request-ID: req-123
{
"name": "my-project",
"owner_id": "user-456",
"is_private": true
}
Правила:
- Content-Type: application/json для JSON
- Заголовки для метаданных (auth, request ID)
- Тело для данных ресурса
- Query параметры для фильтрации и пагинации
Проектирование ответов
Структура ответа:
HTTP/1.1 201 Created
Content-Type: application/json
X-Request-ID: req-123
{
"id": "repo-789",
"name": "my-project",
"owner_id": "user-456",
"is_private": true,
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}
Правила:
- Content-Type: application/json
- Последовательные имена полей (snake_case)
- ISO 8601 для дат
- UUID для идентификаторов
- Не возвращать внутренние поля
Пагинация
Для списков используем cursor-based или offset-based пагинацию:
Offset-based:
GET /api/v1/repos?limit=20&offset=40
Ответ:
{
"items": [...],
"total": 150,
"limit": 20,
"offset": 40
}
Cursor-based (рекомендуется для больших датасетов):
GET /api/v1/repos?limit=20&cursor=eyJpZCI6MTAwfQ==
Ответ:
{
"items": [...],
"next_cursor": "eyJpZCI6MTIwfQ==",
"has_more": true
}
Фильтрация и сортировка
GET /api/v1/repos?owner_id=user-456&is_private=true
GET /api/v1/repos?sort=created_at&order=desc
GET /api/v1/repos?status=active&page=1&per_page=20
1.6 Формат ошибок: RFC 7807 Problem Details
Зачем стандартный формат ошибок
Клиенты должны единообразно обрабатывать ошибки. Стандартный формат упрощает:
- Обработку ошибок на клиенте
- Логирование и мониторинг
- Документацию API
RFC 7807 Problem Details
Стандарт RFC 7807 определяет формат ошибок:
json{ "type": "https://api.example.com/errors/not-found", "title": "Resource Not Found", "status": 404, "detail": "Repository with id 'repo-123' was not found", "instance": "/api/v1/repos/repo-123" }
Поля:
| Поле | Назначение | Обязательное |
|---|---|---|
| type | URI идентифицирующий тип ошибки | Нет |
| title | Краткое описание типа ошибки | Да |
| status | HTTP код ответа | Да |
| detail | Описание конкретной ошибки | Нет |
| instance | URI конкретного запроса | Нет |
Расширенные поля
Для валидации добавляем поле errors:
json{ "type": "https://api.example.com/errors/validation", "title": "Validation Error", "status": 400, "detail": "Request validation failed", "errors": [ { "field": "name", "message": "name must be between 3 and 100 characters" }, { "field": "owner_id", "message": "owner_id must be a valid UUID" } ] }
Реализация в Go
gotype ProblemDetail struct { Type string `json:"type,omitempty"` Title string `json:"title"` Status int `json:"status"` Detail string `json:"detail,omitempty"` Instance string `json:"instance,omitempty"` Errors []FieldError `json:"errors,omitempty"` } type FieldError struct { Field string `json:"field"` Message string `json:"message"` } func writeProblem(w http.ResponseWriter, r *http.Request, status int, title, detail string) { problem := ProblemDetail{ Type: fmt.Sprintf("https://api.example.com/errors/%s", strings.ToLower(strings.ReplaceAll(title, " ", "-"))), Title: title, Status: status, Detail: detail, Instance: r.URL.Path, } w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(status) json.NewEncoder(w).Encode(problem) }
Использование
gofunc (h *Handler) GetRepo(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") repo, err := h.usecase.Execute(r.Context(), id) if err != nil { switch { case errors.Is(err, service.ErrRepoNotFound): writeProblem(w, r, http.StatusNotFound, "Resource Not Found", fmt.Sprintf("Repository with id '%s' was not found", id)) default: writeProblem(w, r, http.StatusInternalServerError, "Internal Server Error", "An unexpected error occurred") } return } h.writeJSON(w, http.StatusOK, repo) }
1.7 OpenAPI спецификация
Что такое OpenAPI
OpenAPI (ранее Swagger) - стандарт описания REST API. Позволяет:
- Документировать API
- Генерировать клиентские SDK
- Генерировать серверный код
- Тестировать API
- Визуализировать API (Swagger UI)
Базовая структура
yamlopenapi: "3.0.3" info: title: Repo Service API version: "1.0.0" description: API for managing repositories servers: - url: https://api.example.com/api/v1 paths: /repos: get: summary: List repositories operationId: listRepos parameters: - name: limit in: query schema: type: integer responses: "200": description: List of repositories content: application/json: schema: type: object properties: items: type: array items: $ref: "#/components/schemas/Repo" post: summary: Create repository operationId: createRepo requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateRepoRequest" responses: "201": description: Repository created content: application/json: schema: $ref: "#/components/schemas/Repo" components: schemas: Repo: type: object properties: id: type: string format: uuid name: type: string owner_id: type: string format: uuid created_at: type: string format: date-time CreateRepoRequest: type: object required: - name - owner_id properties: name: type: string minLength: 3 maxLength: 100 owner_id: type: string format: uuid is_private: type: boolean
Contract-first подход
- Сначала пишем OpenAPI спецификацию
- Генерируем серверный код или пишем вручную по спецификации
- Клиенты используют спецификацию для генерации SDK
- Изменения API начинаются с изменения спецификации
Преимущества:
- Контракт определён до реализации
- Документация всегда актуальна
- Клиенты могут работать параллельно
- Легче ревьюить изменения API
Пример из шаблона: api/openapi.yaml
В шаблоне проекта OpenAPI спецификация хранится в api/openapi.yaml. Каждый эндпоинт описан с request/response схемами.
Привязка к шаблону проекта
Где API Design в шаблоне
textapi/ └── openapi.yaml # OpenAPI спецификация internal/handlers/http/api/v1/ ├── handler.go # HTTP хэндлеры ├── dto.go # DTO с контрактами ├── mapper.go # Мапперы DTO <-> Domain └── routes.go # Регистрация роутов
Что изучаем на примере шаблона
- URL структура: /api/v1/{resource}
- HTTP методы для каждого эндпоинта
- Коды ответов в handleError
- DTO как контракты запросов/ответов
- OpenAPI спецификация
Задание: изучите api/openapi.yaml
- Какие эндпоинты описаны?
- Какие схемы используются?
- Есть ли описание ошибок?
- Совпадает ли спецификация с реализацией?
💱 Компромиссы
- PUT vs PATCH:
PUTидемпотентен и безопасен при retry, но требует всё тело целиком и сбрасывает не переданные поля;PATCHэкономит трафик и меняет только нужное, но не идемпотентен и сложнее описывается в контракте. - URL vs Header версионирование:
/api/v1/видно в логах и легко тестировать, но при смене версии меняется сам URL; заголовокAccept: application/vnd.myapi.v2+jsonдержит URL чистым, но версию не видно в логах и отладка сложнее. - Offset vs cursor пагинация:
offsetпрост и позволяет прыгнуть на нужную страницу, ноOFFSETсканирует все пропускаемые строки и список «плывёт» при вставках;cursorстабилен и быстр, но листать можно только подряд. - RFC 7807 vs простой
{"error": "..."}: Problem Details даёт клиенту машиночитаемыеtype,detailи списокerrors, но требует отдельного Content-Type и поддержки на клиенте; однострочный JSON компактнее, но каждый API изобретает свой формат ошибок. - Contract-first vs code-first: OpenAPI до кода позволяет клиенту и серверу работать параллельно, но каждое изменение начинается с правки YAML; code-first быстрее стартует, но документация расходится с реализацией.
Практическое задание
Задание 1: Проектирование URL
Спроектируйте URL для API управления проектами:
- Создать проект
- Получить проект по ID
- Список проектов с фильтрацией
- Обновить проект
- Удалить проект
- Список задач проекта
- Добавить задачу к проекту
Задание 2: HTTP методы и коды
Для каждого эндпоинта определите:
- HTTP метод
- Код успешного ответа
- Возможные коды ошибок
Пример:
textPOST /api/v1/projects Success: 201 Created Errors: 400 (валидация), 401 (не аутентифицирован), 409 (дубликат)
Задание 3: Реализация Problem Details
Реализуйте writeProblem и используйте для всех ошибок:
gofunc writeProblem(w http.ResponseWriter, r *http.Request, status int, title, detail string) func writeValidationProblem(w http.ResponseWriter, r *http.Request, errors []FieldError)
Задание 4: Пагинация
Реализуйте cursor-based пагинацию для списка репозиториев:
GET /api/v1/repos?limit=20&cursor=xxx
Ответ:
{
"items": [...],
"next_cursor": "...",
"has_more": true
}
Задание 5: OpenAPI спецификация
Напишите OpenAPI спецификацию для Task Manager API из Bridge Course:
- POST /api/v1/tasks
- GET /api/v1/tasks/{id}
- GET /api/v1/tasks
- PUT /api/v1/tasks/{id}
- DELETE /api/v1/tasks/{id}
Задание 6: Версионирование
Добавьте поддержку v2 API для одного эндпоинта:
textv1: GET /api/v1/repos/{id} - возвращает базовые поля v2: GET /api/v2/repos/{id} - возвращает расширенные поля (+ statistics)
Quiz "Проверь себя"
{
"type": "https://api.example.com/errors/validation",
"title": "Validation failed",
"status": 422,
"detail": "name must be 3-100 characters",
"instance": "/api/v1/repos"
}PATCH /api/v1/repos/42
{"description": "новое описание"}✅ Итоги модуля
Студент должен:
- Проектировать REST API по принципам ресурсоориентированности
- Выбирать правильные HTTP методы и коды ответов
- Реализовывать версионирование API
- Проектировать контракты запросов и ответов
- Использовать RFC 7807 для ошибок
- Писать OpenAPI спецификации
- Ответить на все вопросы Quiz
🔗 Мостик к Модулю 2
Следующий модуль: Модуль 2. Виды организации проектов - Flat structure, Layered, Clean Architecture, Hexagonal, Modular Monolith, Microservices. Сравнение и критерии выбора. Привязка к структуре internal/ шаблона.