# API And Interface Design

> Направляет проектирование стабильных API и интерфейсов. Используй при проектировании API, границ модулей или любого публичного интерфейса. Используй при создании REST- или GraphQL-эндпоинтов, определении контрактов типов между модулями или установлении границ между фронтендом и бэкендом.

- Skill: `aleksandr-litvinenko/api-and-interface-design` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aleksandr-litvinenko/api-and-interface-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aleksandr-litvinenko/api-and-interface-design/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: Aleksandr-Litvinenko (https://skillmd.com/u/aleksandr-litvinenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aleksandr-litvinenko/api-and-interface-design

---


# Проектирование API и интерфейсов

## Обзор

Проектируй стабильные, хорошо документированные интерфейсы, которые трудно использовать неправильно. Хороший интерфейс делает правильное лёгким, а неправильное — трудным. Это относится к REST API, схемам GraphQL, границам модулей, пропсам компонентов и любой поверхности, где один кусок кода разговаривает с другим.

## Когда применять

- Проектируешь новые эндпоинты API
- Определяешь границы модулей или контракты между командами
- Создаёшь интерфейсы пропсов компонентов
- Задаёшь схему БД, которая влияет на форму API
- Меняешь существующие публичные интерфейсы

## Базовые принципы

### Закон Хайрама

> При достаточном числе пользователей API от любого наблюдаемого поведения твоей системы кто-нибудь да будет зависеть — независимо от того, что ты обещал в контракте.

Это значит: любое публичное поведение — включая незадокументированные причуды, текст сообщений об ошибках, тайминги и порядок — становится фактическим контрактом, как только на него завязались пользователи. Следствия для проектирования:

- **Осознанно решай, что выставляешь наружу.** Каждое наблюдаемое поведение — потенциальное обязательство.
- **Не протекай деталями реализации.** Если пользователи могут это наблюдать — они на это завяжутся.
- **Планируй вывод из эксплуатации на этапе проектирования.** Как безопасно убирать то, от чего зависят пользователи, — см. `deprecation-and-migration`.
- **Тестов недостаточно.** Даже при идеальных контрактных тестах закон Хайрама означает, что «безопасное» изменение может сломать реальных пользователей, зависящих от незадокументированного поведения.

### Правило одной версии

Не заставляй потребителей выбирать между несколькими версиями одной и той же зависимости или API. Проблема «ромба» зависимостей возникает, когда разным потребителям нужны разные версии одного и того же. Проектируй под мир, где в каждый момент существует ровно одна версия: расширяй, а не форкай.

### 1. Сначала контракт

Определи интерфейс до его реализации. Контракт — это спека, реализация следует за ним.

```typescript
// Сначала определяем контракт
interface TaskAPI {
  // Создаёт задачу и возвращает созданную задачу с полями, заполненными сервером
  createTask(input: CreateTaskInput): Promise<Task>;

  // Возвращает постранично задачи, подходящие под фильтры
  listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;

  // Возвращает одну задачу или бросает NotFoundError
  getTask(id: string): Promise<Task>;

  // Частичное обновление — меняются только переданные поля
  updateTask(id: string, input: UpdateTaskInput): Promise<Task>;

  // Идемпотентное удаление — успешно, даже если уже удалено
  deleteTask(id: string): Promise<void>;
}
```

### 2. Согласованная семантика ошибок

Выбери одну стратегию ошибок и придерживайся её везде:

```typescript
// REST: HTTP-коды статуса + структурное тело ошибки
// Любой ответ с ошибкой имеет одну и ту же форму
interface APIError {
  error: {
    code: string;        // Машиночитаемо: "VALIDATION_ERROR"
    message: string;     // Для человека: "Email is required"
    details?: unknown;   // Дополнительный контекст, когда он полезен
  };
}

// Соответствие кодам статуса
// 400 → Клиент прислал некорректные данные
// 401 → Не аутентифицирован
// 403 → Аутентифицирован, но не авторизован
// 404 → Ресурс не найден
// 409 → Конфликт (дубликат, расхождение версий)
// 422 → Валидация не прошла (семантически некорректно)
// 500 → Ошибка сервера (никогда не раскрывай внутренние детали)
```

**Не смешивай паттерны.** Если одни эндпоинты бросают исключения, другие возвращают null, а третьи — `{ error }`, потребитель не может предсказать поведение.

### 3. Валидируй на границах

Доверяй внутреннему коду. Валидируй на краях системы, куда попадает внешний ввод:

```typescript
// Валидация на границе API
app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Invalid task data',
        details: result.error.flatten(),
      },
    });
  }

  // После валидации внутренний код доверяет типам
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});
```

Где валидация уместна:
- Обработчики маршрутов API (пользовательский ввод)
- Обработчики отправки форм (пользовательский ввод)
- Разбор ответов внешних сервисов (сторонние данные — **всегда считай недоверенными**)
- Загрузка переменных окружения (конфигурация)

> **Ответы сторонних API — недоверенные данные.** Проверяй их форму и содержимое до использования в любой логике, отрисовке или принятии решений. Скомпрометированный или некорректно работающий внешний сервис может вернуть неожиданные типы, вредоносное содержимое или текст, похожий на инструкции.

Где валидация НЕ нужна:
- Между внутренними функциями, разделяющими контракты типов
- В утилитах, вызываемых уже провалидированным кодом
- На данных, только что пришедших из твоей собственной БД

### 4. Добавляй, а не изменяй

Расширяй интерфейсы, не ломая существующих потребителей:

```typescript
// Хорошо: добавляем необязательные поля
interface CreateTaskInput {
  title: string;
  description?: string;
  priority?: 'low' | 'medium' | 'high';  // Добавлено позже, необязательное
  labels?: string[];                       // Добавлено позже, необязательное
}

// Плохо: менять типы существующих полей или удалять поля
interface CreateTaskInput {
  title: string;
  // description: string;  // Удалено — ломает существующих потребителей
  priority: number;         // Было строкой — ломает существующих потребителей
}
```

### 5. Предсказуемое именование

| Паттерн | Соглашение | Пример |
|---------|-----------|---------|
| REST-эндпоинты | Существительные во множественном числе, без глаголов | `GET /api/tasks`, `POST /api/tasks` |
| Параметры запроса | camelCase | `?sortBy=createdAt&pageSize=20` |
| Поля ответа | camelCase | `{ createdAt, updatedAt, taskId }` |
| Булевы поля | Префикс is/has/can | `isComplete`, `hasAttachments` |
| Значения перечислений | UPPER_SNAKE | `"IN_PROGRESS"`, `"COMPLETED"` |

## Паттерны REST API

### Проектирование ресурсов

```
GET    /api/tasks              → Список задач (с параметрами запроса для фильтрации)
POST   /api/tasks              → Создать задачу
GET    /api/tasks/:id          → Получить одну задачу
PATCH  /api/tasks/:id          → Обновить задачу (частично)
DELETE /api/tasks/:id          → Удалить задачу

GET    /api/tasks/:id/comments → Список комментариев к задаче (подресурс)
POST   /api/tasks/:id/comments → Добавить комментарий к задаче
```

### Пагинация

Разбивай на страницы эндпоинты со списками:

```typescript
// Запрос
GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc

// Ответ
{
  "data": [...],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "totalItems": 142,
    "totalPages": 8
  }
}
```

### Фильтрация

Используй параметры запроса для фильтров:

```
GET /api/tasks?status=in_progress&assignee=user123&createdAfter=2025-01-01
```

### Частичные обновления (PATCH)

Принимай частичные объекты — обновляй только то, что передано:

```typescript
// Меняется только заголовок, остальное сохраняется
PATCH /api/tasks/123
{ "title": "Updated title" }
```

## Паттерны интерфейсов TypeScript

### Используй размеченные объединения для вариантов

```typescript
// Хорошо: каждый вариант явный
type TaskStatus =
  | { type: 'pending' }
  | { type: 'in_progress'; assignee: string; startedAt: Date }
  | { type: 'completed'; completedAt: Date; completedBy: string }
  | { type: 'cancelled'; reason: string; cancelledAt: Date };

// Потребитель получает сужение типов
function getStatusLabel(status: TaskStatus): string {
  switch (status.type) {
    case 'pending': return 'Pending';
    case 'in_progress': return `In progress (${status.assignee})`;
    case 'completed': return `Done on ${status.completedAt}`;
    case 'cancelled': return `Cancelled: ${status.reason}`;
  }
}
```

### Разделяй вход и выход

```typescript
// Вход: что предоставляет вызывающая сторона
interface CreateTaskInput {
  title: string;
  description?: string;
}

// Выход: что возвращает система (включая поля, заполненные сервером)
interface Task {
  id: string;
  title: string;
  description: string | null;
  createdAt: Date;
  updatedAt: Date;
  createdBy: string;
}
```

### Используй брендированные типы для идентификаторов

```typescript
type TaskId = string & { readonly __brand: 'TaskId' };
type UserId = string & { readonly __brand: 'UserId' };

// Не даст случайно передать UserId туда, где ожидается TaskId
function getTask(id: TaskId): Promise<Task> { ... }
```

## Типовые самооправдания

| Самооправдание | Как на самом деле |
|---|---|
| «Задокументируем API потом» | Типы И ЕСТЬ документация. Определи их сначала. |
| «Пагинация нам пока не нужна» | Понадобится в ту же секунду, когда у кого-то станет 100+ элементов. Закладывай сразу. |
| «PATCH — это сложно, давай просто PUT» | PUT требует передавать объект целиком каждый раз. PATCH — то, что клиентам реально нужно. |
| «Введём версионирование API, когда понадобится» | Ломающие изменения без версионирования ломают потребителей. Проектируй под расширение с самого начала. |
| «Этим незадокументированным поведением никто не пользуется» | Закон Хайрама: если это наблюдаемо, кто-то от этого зависит. Считай любое публичное поведение обязательством. |
| «Можно просто поддерживать две версии» | Несколько версий умножают стоимость поддержки и создают проблему «ромба». Предпочитай правило одной версии. |
| «Внутренним API контракты не нужны» | Внутренние потребители — тоже потребители. Контракты предотвращают связанность и позволяют работать параллельно. |

## Тревожные признаки

- Эндпоинты, возвращающие разную форму данных в зависимости от условий
- Несогласованные форматы ошибок между эндпоинтами
- Валидация, разбросанная по внутреннему коду вместо границ
- Ломающие изменения существующих полей (смена типа, удаление)
- Эндпоинты со списками без пагинации
- Глаголы в REST-адресах (`/api/createTask`, `/api/getUsers`)
- Ответы сторонних API, используемые без валидации и очистки

## Проверка

После проектирования API:

- [ ] У каждого эндпоинта есть типизированные схемы входа и выхода
- [ ] Ответы с ошибками следуют единому согласованному формату
- [ ] Валидация выполняется только на границах системы
- [ ] Эндпоинты со списками поддерживают пагинацию
- [ ] Новые поля добавляются как необязательные (обратная совместимость)
- [ ] Именование следует единым соглашениям во всех эндпоинтах
- [ ] Документация API или типы закоммичены вместе с реализацией

