Олег Марков
Public API - public-api - подробное руководство для разработчиков
Введение
Public API (public-api) — это внешний интерфейс приложения или сервиса, предназначенный для использования другими системами и разработчиками. Через него клиентские приложения, партнерские сервисы, мобильные клиенты, микросервисы и интеграции получают доступ к функциям и данным вашей системы.
Смотрите, я покажу вам, как на Public API смотрят с точки зрения архитектуры, как им пользоваться и как его правильно проектировать. В статье мы будем опираться в первую очередь на HTTP‑API (REST и близкие к нему стили), потому что именно их чаще всего имеют в виду под public-api в веб-разработке.
Давайте по шагам разберем:
- какие бывают типы Public API;
- как выглядит типичная структура запросов и ответов;
- как организовать аутентификацию и авторизацию;
- как проектировать ресурсы и методы;
- как документировать и версионировать public-api;
- какие практики помогут сделать интерфейс стабильным и безопасным.
Что такое Public API и чем он отличается от других API
Определение Public API
Public API — это программный интерфейс, который:
- документирован и предназначен для внешних пользователей;
- имеет стабильный договор (контракт) по структуре входящих и исходящих данных;
- защищен (как минимум по аутентификации и авторизации);
- поддерживается и развивается по понятным правилам (версионирование, депрекейшн).
Важно разделять понятия:
- внутренний (private, internal) API — используется только внутри одной команды или внутри периметра компании;
- партнерский API — формально может быть внешним, но доступ выдается ограниченному числу партнеров;
- публичный API (public-api) — доступен широкой аудитории разработчиков по понятной процедуре (например, регистрация и получение ключа).
Public API почти всегда строят с учетом:
- обратной совместимости — вы не можете «сломать» интеграции сотен клиентов простым изменением поля;
- четкой политики версионирования;
- повышенных требований к безопасности и отказоустойчивости.
Типы Public API по протоколу
Чаще всего под public-api понимают:
- RESTful HTTP API — классический вариант с ресурсами и методами HTTP;
- JSON-over-HTTP — близкий к REST стиль, но без строго следования всем REST‑принципам;
- GraphQL API — единая схема и один HTTP‑эндпоинт с декларативными запросами;
- gRPC API — больше подходит для межсервисного взаимодействия, но иногда используется и как публичный.
В этой статье мы сосредоточимся на HTTP Public API с JSON, потому что это самый распространенный и понятный формат для разработчиков.
Базовая структура Public API
Базовый URL и версия
Обычно Public API доступен по базовому URL:
Версия часто указывается:
- в URL, например
https://api.example.com/v1/users - или в заголовках, например
X-API-Version: 1
Для большинства сценариев проще начинать с версии в URL. Смотрите, как это может выглядеть в целом:
- GET https://api.example.com/v1/users — список пользователей;
- POST https://api.example.com/v1/users — создание пользователя;
- GET https://api.example.com/v1/users/{id} — получение пользователя по идентификатору.
Формат данных
Наиболее распространенный формат — JSON. Клиент сообщает, что ожидает JSON, через заголовок Accept, а сервер возвращает Content-Type.
Пример простого ответа Public API:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"email": "user@example.com",
"name": "Ivan Petrov",
"created_at": "2025-01-01T12:00:00Z"
}
Здесь я использую поля в snake_case, но вы можете выбрать и camelCase. Главное — быть последовательными в рамках всего public-api.
Стандартные методы HTTP
Чаще всего используются:
- GET — получение данных;
- POST — создание ресурса или выполнение действия;
- PUT — полное обновление ресурса;
- PATCH — частичное обновление;
- DELETE — удаление.
Например:
GET /v1/users # Получить список пользователей
GET /v1/users/123 # Получить одного пользователя
POST /v1/users # Создать пользователя
PATCH /v1/users/123 # Частично обновить пользователя
DELETE /v1/users/123 # Удалить пользователя
Комментарии здесь показывают тип операций, которые вы выполняете на одном и том же ресурсе users.
Аутентификация и авторизация в Public API
Токены и ключи API
Почти любой public-api требует аутентификацию. Самые распространенные варианты:
- API Key — простой ключ, который передается в заголовках или в параметрах;
- OAuth 2.0 — более сложная, но гибкая схема с access token;
- JWT (JSON Web Token) — токен формата JWT, подписанный сервером.
Пример с API Key в заголовке:
GET /v1/users/me HTTP/1.1
Host: api.example.com
X-API-Key: your_api_key_here
Accept: application/json
В этом примере:
- X-API-Key — пользовательский заголовок, который ваш сервер проверяет;
- yourapikey_here — секретное значение, которое клиент получает при регистрации.
Пример с Bearer токеном (OAuth 2.0 или JWT):
GET /v1/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Accept: application/json
Здесь:
- Authorization — стандартный заголовок;
- Bearer
— схема, которая говорит, что вы передаете токен доступа.
Ответы при ошибках авторизации
Хорошая практика — всегда возвращать понятные коды ошибок и сообщения.
Пример отсутствия токена:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "unauthorized",
"error_description": "Missing or invalid authentication token"
}
Пример недостаточных прав (нет доступа к ресурсу):
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "forbidden",
"error_description": "You do not have access to this resource"
}
Обратите внимание, как текст в error_description объясняет, что именно произошло. Это сильно облегчает отладку для тех, кто интегрируется с вашим public-api.
Проектирование ресурсов и эндпоинтов
Ресурсный подход
В ресурсном public-api вы работаете с сущностями:
- users
- orders
- products
- invoices
- etc.
Именно эти сущности становятся основой URL. Давайте разберем пример для ресурса orders.
Возможная структура:
- GET /v1/orders — список заказов;
- POST /v1/orders — создание заказа;
- GET /v1/orders/{id} — один заказ;
- PATCH /v1/orders/{id} — частичное обновление;
- DELETE /v1/orders/{id} — удаление заказа.
Теперь вы увидите, как это выглядит в примере JSON.
Создание заказа:
POST /v1/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json
{
"customer_id": 42, // Идентификатор клиента
"items": [ // Список позиций заказа
{
"product_id": 10, // Идентификатор товара
"quantity": 2 // Количество
},
{
"product_id": 11,
"quantity": 1
}
],
"comment": "Please deliver after 18:00" // Комментарий
}
Пример ответа:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/orders/1001
{
"id": 1001, // Созданный идентификатор заказа
"status": "pending", // Статус заказа
"customer_id": 42,
"items": [
{
"product_id": 10,
"quantity": 2
},
{
"product_id": 11,
"quantity": 1
}
],
"comment": "Please deliver after 18:00",
"created_at": "2025-01-10T12:30:00Z"
}
Как видите, сервер возвращает не только id, но и все важные поля, которые могут понадобиться клиенту сразу после создания.
Фильтрация, сортировка, пагинация
Public API обычно должен уметь эффективно возвращать большие списки. Для этого используются:
- query‑параметры для фильтрации и сортировки;
- параметры пагинации (page, limit, cursor и т. д.).
Пример запроса списка с фильтрами:
GET /v1/orders?status=pending&customer_id=42&sort=-created_at&page=1&limit=20 HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Accept: application/json
Здесь:
- status=pending — фильтруем по статусу;
- customer_id=42 — фильтр по клиенту;
- sort=-created_at — сортировка по дате создания по убыванию (минус обозначает убывание);
- page=1, limit=20 — первая страница по 20 записей.
Пример ответа с пагинацией:
{
"data": [
{
"id": 1001,
"status": "pending",
"customer_id": 42,
"created_at": "2025-01-10T12:30:00Z"
}
// Здесь могут быть другие заказы
],
"meta": {
"page": 1, // Текущая страница
"limit": 20, // Размер страницы
"total": 135, // Общее количество элементов
"total_pages": 7 // Общее количество страниц
}
}
Комментарии подсказывают, как интерпретировать метаинформацию, чтобы вы могли правильно построить клиентский код.
Действия над ресурсами
Иногда вам нужно представить не только CRUD‑операции, но и действия, которые меняют состояние ресурса. Например:
- подтверждение заказа;
- отмена заказа;
- смена пароля;
Есть два распространенных подхода:
Отдельный эндпоинт:
- POST /v1/orders/{id}/confirm
- POST /v1/orders/{id}/cancel
Поле состояния и PATCH:
- PATCH /v1/orders/{id} с телом {"status": "confirmed"}
Чаще для публичных API используют первый вариант для явных действий, чтобы не оставлять место для неправильных комбинаций состояний.
Пример:
POST /v1/orders/1001/confirm HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Accept: application/json
Ответ:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1001,
"status": "confirmed", // Статус изменился
"confirmed_at": "2025-01-10T13:00:00Z"
}
Обратите внимание, как отдельное поле confirmed_at явно показывает момент подтверждения.
Единый формат ошибок в Public API
Зачем нужен единый формат
Если вы хотите, чтобы сторонние разработчики могли быстро отладить интеграцию, ошибки должны быть:
- предсказуемыми по структуре;
- понятными по коду и сообщению;
- детальными там, где это безопасно.
Давайте разберем типичный формат ошибок:
{
"error": "validation_error", // Код общего типа ошибки
"error_description": "Validation failed", // Краткое описание
"details": [ // Детали по конкретным полям
{
"field": "email",
"message": "Email is invalid"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
],
"request_id": "b12f90c1-4bde-4b2c-9c38-9aa9d2b9a001" // Идентификатор запроса для логов
}
Теперь давайте посмотрим, как это выглядит в HTTP‑ответе:
HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Request-Id: b12f90c1-4bde-4b2c-9c38-9aa9d2b9a001
{
"error": "validation_error",
"error_description": "Validation failed",
"details": [
{
"field": "email",
"message": "Email is invalid"
}
],
"request_id": "b12f90c1-4bde-4b2c-9c38-9aa9d2b9a001"
}
Комментарии в теле ответа уже были выше, поэтому здесь вы легко можете сопоставить их с заголовком X-Request-Id.
Основные коды ошибок
Рекомендуется использовать стандартные HTTP‑коды:
- 400 — неверный запрос (validationerror, invalidrequest);
- 401 — неавторизован (unauthorized);
- 403 — нет прав (forbidden);
- 404 — не найдено (not_found);
- 409 — конфликт (conflict, например, дубликат уникального поля);
- 429 — слишком много запросов (ratelimitexceeded);
- 500 — ошибка сервера (internal_error);
- 503 — сервис недоступен (service_unavailable).
Покажу вам простой пример ошибки лимита запросов:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
{
"error": "rate_limit_exceeded",
"error_description": "Rate limit exceeded. Try again later",
"retry_after": 60 // Через сколько секунд можно повторить запрос
}
Такой ответ сразу дает клиенту понятную инструкцию, как корректно обработать ситуацию.
Версионирование Public API
Зачем нужно версионирование
Когда вы публикуете внешний API, вы заключаете договор с пользователями. Любое изменение, которое может «сломать» существующий код клиентов, считается несовместимым (breaking change). Чтобы все было управляемо, используют версии.
Варианты:
- v1, v2 в URL;
- semver, отраженный в URL или заголовках;
- тип «вечный v1», где вы добавляете только обратно совместимые изменения.
Что считать breaking change
Несколько примеров несовместимых изменений:
- удаление поля из ответа;
- изменение типа поля (было число, стало строка);
- изменение семантики поля (значения меняют смысл);
- изменение структуры тела запроса;
- изменение кода ответа (например, 200 вместо 201).
То, что вы добавляете новое поле в ответ — обычно не является breaking change, если клиенты умеют его игнорировать. Но здесь важно зафиксировать это в документации как правило.
Пример двух версий
Представим:
- v1 — возвращает пользователя с полем full_name;
- v2 — вместо fullname возвращает firstname и last_name.
Возможная реализация:
GET /v1/users/123 HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Accept: application/json
Ответ v1:
{
"id": 123,
"email": "user@example.com",
"full_name": "Ivan Petrov"
}
Теперь новая версия:
GET /v2/users/123 HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Accept: application/json
Ответ v2:
{
"id": 123,
"email": "user@example.com",
"first_name": "Ivan", // Имя
"last_name": "Petrov" // Фамилия
}
Здесь v1 и v2 существуют параллельно, и клиенты сами выбирают, когда мигрировать.
Документация Public API
Почему документация — часть продукта
Для public-api документация — не просто дополнение, а основной инструмент, по которому судят о качестве всего сервиса. Если документация:
- полная;
- актуальная;
- содержит примеры запросов и ответов;
- описывает коды ошибок;
то разработчикам будет гораздо проще интегрироваться.
Чаще всего используют:
- OpenAPI (Swagger) — машинно‑читаемое описание + визуальный UI;
- встроенную документацию в виде API портала;
- примеры SDK и кода на популярных языках.
Пример фрагмента OpenAPI
Ниже я показываю упрощенный пример для эндпоинта GET /v1/users/{id}:
paths:
/v1/users/{id}:
get:
summary: Get user by id # Краткое описание
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: User found
content:
application/json:
schema:
type: object
properties:
id:
type: integer
email:
type: string
name:
type: string
'404':
description: User not found
Комментарии здесь помогают вам понять, какую роль играет каждый блок.
Примеры для клиентов
Очень полезно в документации показывать «готовые» запросы:
- curl;
- фрагменты на JavaScript, Python, Go, Java и т. д.
Давайте разберем пример с curl:
curl -X GET "https://api.example.com/v1/users/123" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Accept: application/json"
# Здесь мы отправляем GET-запрос на получение пользователя 123,
# передаем токен в заголовке Authorization и ожидаем JSON-ответ
И пример на JavaScript с fetch:
async function getUser(userId, token) {
const response = await fetch(`https://api.example.com/v1/users/${userId}`, {
method: "GET", // Метод запроса
headers: {
"Authorization": `Bearer ${token}`, // Токен доступа
"Accept": "application/json" // Ожидаемый тип ответа
}
});
if (!response.ok) {
// Здесь мы обрабатываем ошибку, если статус ответа не 2xx
const errorBody = await response.json();
throw new Error(errorBody.error_description || "Request failed");
}
// Если все хорошо - парсим тело ответа как JSON
return response.json();
}
Такой пример можно практически сразу использовать в реальном проекте.
Безопасность Public API
HTTPS как обязательное требование
Все запросы к public-api должны идти только по HTTPS. Это защищает:
- токены и ключи;
- пользовательские данные;
- конфиденциальную бизнес-информацию.
Обычно:
- HTTP перенаправляют на HTTPS с кодом 301 или 308;
- или полностью блокируют HTTP с кодом 400/426.
Ограничение частоты запросов (Rate limiting)
Публичный API подвержен риску:
- чрезмерного потребления ресурсов;
- простых DoS‑атак;
- неаккуратных клиентов, которые делают слишком много запросов.
Поэтому на уровне API часто вводят лимиты, например:
- не более 1000 запросов в минуту на токен;
- дополнительные ограничения на «тяжелые» операции.
Сервер может возвращать специальные заголовки:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000 # Максимальное количество запросов за период
X-RateLimit-Remaining: 750 # Остаток запросов в текущем периоде
X-RateLimit-Reset: 1700000000 # Время UNIX, когда лимит будет сброшен
Покажу вам, как клиент может использовать эти заголовки:
// Здесь мы предполагаем, что у нас есть объект response из fetch
const limit = response.headers.get("X-RateLimit-Limit");
const remaining = response.headers.get("X-RateLimit-Remaining");
const resetAt = response.headers.get("X-RateLimit-Reset");
// По этим значениям клиент может решить, нужно ли замедлить запросы
Защита от утечек данных
Важно не только шифровать трафик, но и:
- фильтровать поля в ответах (не светить внутренние идентификаторы и служебные данные);
- маскировать чувствительные данные (например, часть номера карты);
- не возвращать подробные стеки ошибок и SQL‑сообщения во внешних ответах.
Например, вместо:
{
"error": "internal_error",
"error_description": "SQL error at line 1: syntax error near FROM"
}
лучше вернуть:
{
"error": "internal_error",
"error_description": "Internal server error"
}
А детали уже логировать только во внутреннюю систему мониторинга.
Клиентские SDK и публичный API
Зачем нужны SDK
Public API часто сопровождают:
- официальными SDK (JavaScript, Python, Java, Go и т. д.);
- примерами кода для популярных фреймворков.
SDK позволяют:
- скрыть детали аутентификации;
- инкапсулировать базовый URL, версию API;
- предоставить удобные методы вместо ручной работы с HTTP.
Простой пример класса-клиента на JavaScript:
class PublicApiClient {
constructor({ baseUrl, token }) {
this.baseUrl = baseUrl; // Базовый URL API
this.token = token; // Токен аутентификации
}
async request(path, options = {}) {
const url = `${this.baseUrl}${path}`; // Собираем полный URL
const headers = {
"Authorization": `Bearer ${this.token}`,
"Accept": "application/json",
...options.headers
};
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
// Читаем тело ошибки и выбрасываем исключение
const errorBody = await response.json().catch(() => ({}));
throw new Error(errorBody.error_description || "Request failed");
}
// Возвращаем распарсенное тело ответа
return response.json();
}
// Метод для получения текущего пользователя
getMe() {
return this.request("/v1/users/me", { method: "GET" });
}
// Метод для создания заказа
createOrder(orderData) {
return this.request("/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(orderData) // Сериализуем тело запроса в JSON
});
}
}
Такой SDK избавляет интегратора от необходимости каждый раз собирать HTTP‑запросы вручную.
Управление жизненным циклом Public API
Депрекейшн старых версий
Со временем какие-то версии API перестают соответствовать текущим требованиям. Тогда вы планируете их отключение:
- сначала помечаете версию как deprecated в документации;
- добавляете предупреждающие заголовки;
- спустя оговоренный срок отключаете.
Пример ответа со специальным заголовком:
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: true
Sunset: Wed, 01 Jan 2026 00:00:00 GMT
Link: <https://developer.example.com/migration-guide-v1-to-v2>; rel="deprecation"
{
"id": 123,
"email": "user@example.com",
"full_name": "Ivan Petrov"
}
Здесь:
- Deprecation: true — явный сигнал, что версия устаревает;
- Sunset — дата, когда версия будет отключена;
- Link с rel="deprecation" — ссылка на гайд по миграции.
Мониторинг и логирование
Чтобы поддерживать качество public-api, важно:
- логировать все запросы и ответы (без чувствительных данных в чистом виде);
- иметь корреляционные идентификаторы (например, X-Request-Id);
- отслеживать метрики (количество запросов, ошибки по типам, задержка).
Пример использования X-Request-Id в запросе клиента:
GET /v1/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
X-Request-Id: 1c9bf2e0-996c-4a87-b708-58abdcaf1234
Accept: application/json
Сервер может:
- логировать этот идентификатор;
- возвращать его в ответе.
Это упростит разбор инцидентов, когда интегратор пишет вам и присылает конкретный request_id.
Public API (public-api) — это стабильный и тщательно продуманный контракт между вашим сервисом и внешним миром. Чтобы он был полезным и удобным:
- проектируйте ресурсы и эндпоинты в ресурсном стиле;
- используйте понятную аутентификацию и авторизацию;
- придерживайтесь единого формата ошибок;
- продуманно подходите к версионированию;
- документируйте все аспекты API и поддерживайте документацию актуальной;
- уделяйте внимание безопасности и мониторингу.
Когда вы относитесь к public-api как к самостоятельному продукту, разработчикам гораздо проще его освоить и встроить в свои решения.
Частозадаваемые вопросы
Как правильно передавать большие объемы данных через Public API
Используйте пагинацию или постраничную выборку. Вместо того чтобы возвращать тысячи записей за один запрос, реализуйте параметры limit и page или cursor‑based пагинацию. На стороне клиента делайте последовательные запросы, пока не получите все данные. Для изменений больших массивов лучше создать отдельный асинхронный эндпоинт, который запускает задачу на сервере, а клиент периодически опрашивает статус.
Как обновлять схему JSON без ломания старых клиентов
Добавляйте только новые поля и избегайте изменения типов существующих полей. Старые клиенты обычно игнорируют неизвестные поля. Если нужно изменить тип или семантику поля, добавьте новое поле с другим именем и пометьте старое как deprecated в документации. Полное удаление старого поля делайте только в новой версии API.
Как обрабатывать idempotent запросы в Public API
Для операций, которые клиент может повторять (например, создание заказа при нестабильной сети), используйте идемпотентные ключи. Клиент передает уникальный идентификатор операции в заголовке (например, Idempotency-Key), а сервер хранит результат первой успешной операции и возвращает его при повторных запросах с тем же ключом. Это помогает избежать дублирования объектов.
Как лучше реализовать загрузку файлов через Public API
Практичный вариант — использовать двухшаговую схему. Сначала клиент вызывает API-метод, который возвращает pre-signed URL для загрузки файла в хранилище (например, S3). Затем клиент загружает файл напрямую по этому URL без прохождения через ваш основной API. После загрузки можно вызвать отдельный метод для подтверждения и привязки файла к сущности (например, к профилю пользователя).
Как обеспечить совместную работу версии v1 и v2 в одном домене
Разводите версии по URL, например /v1/... и /v2/.... Общие механизмы аутентификации и лимитов запросов можно использовать для обеих версий. Внутри сервера маршрутизируйте запросы на разные обработчики или микросервисы в зависимости от префикса версии. В документации четко указывайте, какие эндпоинты относятся к какой версии, и дайте отдельные примеры для миграции с v1 на v2.