Цели модуля

После модуля студент сможет:

  • Проектировать REST API по принципам ресурсоориентированности
  • Выбирать правильные HTTP методы и коды ответов
  • Реализовывать стратегии версионирования API
  • Проектировать контракты запросов и ответов
  • Использовать RFC 7807 Problem Details для ошибок
  • Писать OpenAPI спецификации

1.1 Принципы REST

Что такое REST

REST (Representational State Transfer) - архитектурный стиль для проектирования сетевых приложений. Основные принципы:

  1. Ресурсоориентированность: всё является ресурсом
  2. Единообразие интерфейса: стандартные HTTP методы
  3. Stateless: каждый запрос содержит всю необходимую информацию
  4. Клиент-сервер: разделение ответственности
  5. Кэшируемость: ответы могут быть кэшированы
  6. Слоистая система: промежуточные слои прозрачны

Ресурсы и 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

Исключение: действия не моделируемые как состояние ресурса:

text
POST /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 коды ответов

Категории кодов

ДиапазонКатегорияНазначение
1xxInformationalПромежуточные ответы
2xxSuccessУспешная обработка
3xxRedirectionПеренаправление
4xxClient ErrorОшибка клиента
5xxServer 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 RequestsRate limit превышен

Ошибки сервера:

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

Правила выбора кодов

  1. Используйте наиболее специфичный код
  2. Не возвращайте 200 для ошибок
  3. 400 для ошибок формата, 422 для ошибок семантики
  4. 401 когда нет токена, 403 когда токен есть но прав нет
  5. 500 только для непредвиденных ошибок

Пример из шаблона

go
func (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 версионирование

text
GET /api/v1/repos
GET /api/v2/repos

Плюсы:

  • Простота понимания
  • Легко тестировать в браузере
  • Явное разделение

Минусы:

  • URL меняется при смене версии
  • Не совсем RESTful (версия не часть ресурса)

Рекомендуется для большинства проектов.

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

http
GET /api/repos
Accept: application/vnd.myapi.v1+json

GET /api/repos
Accept: application/vnd.myapi.v2+json

Плюсы:

  • URL не меняется
  • Более RESTful

Минусы:

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

Стратегия 3: Query parameter

text
GET /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"
}

Поля:

ПолеНазначениеОбязательное
typeURI идентифицирующий тип ошибкиНет
titleКраткое описание типа ошибкиДа
statusHTTP код ответаДа
detailОписание конкретной ошибкиНет
instanceURI конкретного запросаНет

Расширенные поля

Для валидации добавляем поле 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

go
type 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)
}

Использование

go
func (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)

Базовая структура

yaml
openapi: "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 подход

  1. Сначала пишем OpenAPI спецификацию
  2. Генерируем серверный код или пишем вручную по спецификации
  3. Клиенты используют спецификацию для генерации SDK
  4. Изменения API начинаются с изменения спецификации

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

  • Контракт определён до реализации
  • Документация всегда актуальна
  • Клиенты могут работать параллельно
  • Легче ревьюить изменения API

Пример из шаблона: api/openapi.yaml

В шаблоне проекта OpenAPI спецификация хранится в api/openapi.yaml. Каждый эндпоинт описан с request/response схемами.


Привязка к шаблону проекта

Где API Design в шаблоне

text
api/
└── openapi.yaml                  # OpenAPI спецификация

internal/handlers/http/api/v1/
├── handler.go                    # HTTP хэндлеры
├── dto.go                        # DTO с контрактами
├── mapper.go                     # Мапперы DTO <-> Domain
└── routes.go                     # Регистрация роутов

Что изучаем на примере шаблона

  1. URL структура: /api/v1/{resource}
  2. HTTP методы для каждого эндпоинта
  3. Коды ответов в handleError
  4. DTO как контракты запросов/ответов
  5. 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 метод
  • Код успешного ответа
  • Возможные коды ошибок

Пример:

text
POST /api/v1/projects
Success: 201 Created
Errors: 400 (валидация), 401 (не аутентифицирован), 409 (дубликат)

Задание 3: Реализация Problem Details

Реализуйте writeProblem и используйте для всех ошибок:

go
func 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 для одного эндпоинта:

text
v1: GET /api/v1/repos/{id} - возвращает базовые поля
v2: GET /api/v2/repos/{id} - возвращает расширенные поля (+ statistics)

Quiz "Проверь себя"

1. Что даёт проекту формальный контракт API (OpenAPI)?
2. Что вернёт API после успешного DELETE, если тело ответа не нужно?
3. Какому стандарту соответствует этот ответ об ошибке и какой Content-Type он использует?
json
{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "name must be 3-100 characters",
  "instance": "/api/v1/repos"
}
4. Чем PATCH отличается от PUT при обновлении ресурса?
http
PATCH /api/v1/repos/42
{"description": "новое описание"}
5. Какой способ версионирования API проще в тестировании и виднее в логах?
6. Какие HTTP-методы являются идемпотентными? — выберите все подходящие
7. Что из перечисленного является breaking change в API? — выберите все подходящие
8. Какой код ответа вернёт обработчик при успешном создании ресурса через POST?
9. Как REST предлагает выражать действия над ресурсами?

✅ Итоги модуля

Студент должен:

  • Проектировать REST API по принципам ресурсоориентированности
  • Выбирать правильные HTTP методы и коды ответов
  • Реализовывать версионирование API
  • Проектировать контракты запросов и ответов
  • Использовать RFC 7807 для ошибок
  • Писать OpenAPI спецификации
  • Ответить на все вопросы Quiz

🔗 Мостик к Модулю 2

Следующий модуль: Модуль 2. Виды организации проектов - Flat structure, Layered, Clean Architecture, Hexagonal, Modular Monolith, Microservices. Сравнение и критерии выбора. Привязка к структуре internal/ шаблона.