Дизайн публичного API: как новичку в SaaS продумать версии, пагинацию, лимиты, документацию и простой SDK без лишней сложности.

Публичный API - это стабильный вход в ваш сервис для чужих систем: партнерских продуктов, интеграторов, внутренней автоматизации клиента. В отличие от внутреннего API, он живет дольше, им пользуются люди вне вашей команды, и любая поломка становится их проблемой, а значит и вашей поддержкой.
С публичным API вы растете там, где UI не помогает. Например, бухгалтерия клиента хочет, чтобы счета и статусы оплат автоматически попадали в их систему. Им не нужен ваш интерфейс, им нужен надежный способ забрать данные и отправить команду.
Самые дорогие ошибки в первых интеграциях обычно не в «красоте» эндпоинтов, а в непредсказуемости. Когда сегодня ответ такой, завтра другой, а после релиза мобильного приложения внезапно меняются поля или статусы. Интегратор тратит часы на отладку, вы тратите дни на разбор тикетов, и доверие падает.
Есть решения, которые потом менять особенно больно, потому что они просачиваются в код клиентов и живут там годами: формат идентификаторов и ключевых полей (строка vs число), правила версионирования и политика изменений, способ аутентификации и модель доступов, структура ошибок (коды, сообщения, поля), базовые договоренности про пагинацию и лимиты.
Цели публичного API обычно простые: интеграции с внешними сервисами и автоматизация у клиентов, партнерские сценарии (когда ваш продукт становится частью чужого решения) и ваши же приложения (мобильное, виджеты, расширения, фоновые задачи). Часто именно мобильное приложение первым заставляет навести порядок в контракте.
Когда вы проектируете публичный API с нуля, думайте не про «идеально», а про контракт, который понятен, предсказуем и расширяем без сюрпризов для тех, кто подключится первым.
Хороший дизайн публичного API начинается не с эндпоинтов, а с людей. Один и тот же метод может быть удобным для вашей команды, но мучительным для партнера, который пишет интеграцию в одиночку и хочет предсказуемость.
Обычно у API три группы пользователей: внешние партнеры (маркетплейсы, бухгалтерии, интеграторы), ваши клиенты (чтобы тянуть свои данные в BI или CRM) и внутренняя команда (поддержка, аналитики, автоматизация). Для первого релиза выберите одну главную группу. Остальные получат пользу позже, но не должны диктовать архитектуру сейчас.
Дальше выберите 2-3 сценария, которые реально будут использоваться каждую неделю. Частая ошибка - сразу пытаться покрыть и чтение, и запись, и массовые операции, и вебхуки. Если вы не уверены, начните с чтения данных: оно проще, безопаснее и быстрее для проверки гипотез.
Чтобы не расползтись, зафиксируйте границы. В первом релизе держите 5-10 ключевых операций и пообещайте себе не добавлять «а еще один маленький метод» без явного запроса. Практичный минимум обычно сводится к следующему: список объектов с пагинацией, получение одного объекта по id, создание (только если без этого нельзя), обновление 1-2 самых частых полей и отдельный способ получить статус или события по объекту.
Параллельно задайте планку надежности. Не обязательно обещать SLA на старте, но клиентам нужны стабильные коды ошибок, понятные лимиты и предсказуемые изменения. Если API падает, партнер не «подождет», он просто отключит интеграцию.
Заранее решите, какие данные нельзя отдавать вообще (персональные, внутренние заметки, технические токены) и где нужен аудит. Минимум - логировать, кто и когда запрашивал чувствительные операции, и уметь быстро отозвать доступ. Это особенно важно, если вы выдаете ключи партнерам и даете экспорт наружу, например когда приложение собрано на TakProsto, а клиент просит интеграцию с внешней системой.
Если вы делаете дизайн публичного API впервые, проще всего стартовать с REST. Он понятен большинству интеграторов, хорошо ложится на HTTP и не требует сложных решений на старте.
Начните с ресурсов (существительных), а не с действий. Хорошее правило: имя в множественном числе и стабильный идентификатор, который не меняется со временем. Например: /users, /projects, /invoices, а конкретный объект - /projects/{id}. Идентификатор лучше делать непрозрачным (например, UUID), чтобы вы могли менять внутреннюю структуру данных без риска.
Методы HTTP держите стандартными и предсказуемыми:
GET - получить список или один объект.POST - создать объект или запустить операцию.PATCH - частично обновить (например, сменить статус).DELETE - удалить или пометить как удаленный.PUT лучше избегать, если вы не готовы поддерживать полную замену объекта без потери полей.
С форматом ответов есть два популярных варианта. Первый - отдавать сразу объект: при GET /projects/{id} вернуть JSON проекта. Это проще для клиентов.
Второй - использовать единый «конверт», например { "data": ..., "meta": ... }. Плюс в том, что вы без боли добавите метаданные (пагинацию, лимиты, предупреждения) и сохраните единый стиль. Минус - клиентам нужно писать чуть больше кода. Компромисс для первого релиза: для списков используйте конверт (там мета чаще нужна), а для одиночных объектов можно отдавать прямой объект.
Отдельно продумайте идемпотентность для создания. Реальный сценарий: партнер отправляет POST /invoices, сеть моргнула, он повторил запрос и получил два счета. Решение простое: поддержите заголовок Idempotency-Key (или поле в теле) и храните результат по ключу какое-то время.
Тогда повтор одного и того же запроса вернет тот же ответ, а не создаст дубль. Это резко снижает число спорных ситуаций и ручных разборов в поддержке.
Версия нужна не «на всякий случай», а когда вы делаете ломающее изменение. Если вы расширяете API (добавили новое поле, новый эндпоинт, новый параметр с безопасным значением по умолчанию), это обычно можно выпускать без новой версии. Хороший дизайн публичного API как раз про то, чтобы большинство изменений были расширениями, а не поломками.
Ломающее изменение - это когда старый клиент перестает работать или начинает получать другой смысл данных. Примеры: вы переименовали поле, изменили тип (строка стала числом), поменяли формат дат, сделали обязательным то, что раньше было необязательным.
Самый простой вариант версионирования, который легче всего поддерживать и объяснять, - версия в пути: /v1/.... Заголовки и параметры тоже работают, но чаще дают больше путаницы: сложнее отлаживать, труднее быстро увидеть версию в логах и примерах. Если вы только начинаете, берите /v1 и не усложняйте.
Главное правило совместимости: новые поля добавлять можно, удалять нельзя. Точнее, клиент должен быть готов к тому, что в ответе появятся дополнительные поля, которые он игнорирует. Например, было:
{ "id": "123", "status": "paid" }
Стало:
{ "id": "123", "status": "paid", "paid_at": "2026-01-09T10:20:00Z" }
Такое изменение не ломает аккуратных клиентов.
Планируя v2, заранее решите, сколько времени вы будете поддерживать v1 и как будете предупреждать. Практичный минимум выглядит так: заранее объявить дату окончания поддержки и причины, добавить предупреждение в ответах (заголовок или поле в метаданных), дать короткую инструкцию миграции с примерами и обеспечить период, когда v1 и v2 работают параллельно. После дедлайна оставьте понятную ошибку и контакт для экстренных случаев.
Changelog и заметки о релизах сильно снижают боль у интеграторов. Пишите человеческим языком: что изменилось, кого касается, есть ли действия со стороны клиента. Даже если вы собираете API в TakProsto, дисциплина релиз-заметок и сроков поддержки важнее, чем выбор конкретного инструмента.
Пагинация нужна не для «красоты», а чтобы API не падал от одного запроса. Если отдавать все записи сразу, вы получите длинные ответы, рост времени ответа, высокую нагрузку на базу и неожиданные счета за трафик. Для клиента тоже плохо: он не может быстро показать первые результаты.
Для старта почти всегда достаточно одного выбора: offset или cursor. Offset (page, offset) проще объяснить, но он плохо переживает изменения данных: пока клиент листает, новые записи могут «сдвинуть» страницы, и часть элементов повторится или пропадет. Cursor-пагинация обычно надежнее для SaaS, потому что вы двигаетесь от известной точки вперед и сохраняете предсказуемость.
Сделайте единый формат, чтобы дизайн публичного API выглядел цельно во всех списках. Хороший минимум:
Важно зафиксировать порядок сортировки. Выберите один базовый ключ, например created_at по убыванию, и добавьте вторичный стабильный ключ (id), чтобы порядок был строгим. Тогда страницы не будут «прыгать» даже при одинаковых датах. Фильтры тоже должны быть частью контракта: если клиент меняет фильтр, курсор из старого запроса применять нельзя.
Пример для списка событий или транзакций (в духе «последние операции»):
// GET /v1/transactions?limit=50&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMjoyMzowMFoiLCJpZCI6IjEwMjM0NSJ9
{
"items": [
{"id":"102345","created_at":"2026-01-09T12:23:00Z","amount":1200,"status":"paid"}
],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMjoyMjowMFoiLCJpZCI6IjEwMjM0NCJ9",
"has_more": true
}
Практичные детали, которые экономят время позже: задайте разумный default limit (например 20-50), поставьте верхнюю границу (например 100-200), и всегда возвращайте одинаковую структуру ответа, даже если items пустой.
Rate limits нужны не только против злоупотреблений. Они делают интеграции предсказуемыми: клиент понимает, сколько запросов можно отправить, а вы защищаете базу и фоновые задачи от всплесков.
Сначала решите, что именно ограничивать. Обычно хватает 2-3 измерений: запросы в минуту (или в секунду) на API-ключ или пользователя, одновременные запросы (чтобы один клиент не забрал все воркеры), и отдельные лимиты для тяжелых эндпоинтов (экспорт, отчеты, поиск по большим наборам).
Дальше важно правильно сообщать об ограничении. При превышении возвращайте HTTP 429 Too Many Requests и понятный текст ошибки: что именно превышено и когда можно повторить. Добавьте заголовки, чтобы клиент мог заранее планировать нагрузку: например, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (или X-RateLimit-*, если так проще в вашей экосистеме). И обязательно используйте Retry-After, чтобы клиент не гадал.
Вместо «кирпича» лучше подсказать корректное поведение. Если запросы идут волнами, клиент делает экспоненциальный backoff (1с, 2с, 4с) и повторяет запрос после Retry-After. Для операций, которые можно отложить, можно принять задачу в очередь и вернуть быстрый ответ (например, статус «принято»), а результат отдать позже через отдельный эндпоинт.
Лимиты почти всегда отличаются по планам: free, pro, business, enterprise. Не обязательно выносить все цифры в маркетинг, но в документации API лучше быть точным: «в этом плане N запросов в минуту и M параллельных».
Чтобы не уйти в паранойю, начните с простого лимита на ключ плюс базовая защита от очевидных атак (подозрительные пики, слишком частые ошибки, запросы без auth). Если видите проблему, добавляйте точечные правила на конкретные эндпоинты, а не усложняйте весь API сразу.
Самая частая ошибка в дизайне публичного API на старте - сделать вход слишком сложным и отпугнуть первых интеграторов. Вторая ошибка - оставить все «на одном ключе» без ограничений и потом разгребать утечки и случайные удаления данных. Нужен простой минимум, который можно расширить.
Для большинства молодого SaaS достаточно API key в заголовке запроса (например, Authorization: Bearer <key>). Это быстро объяснить, легко внедрить и удобно тестировать.
Выбор обычно такой:
Если сомневаетесь, начните с API key + скоупы. OAuth2 добавите позже, когда появятся реальные кейсы.
Показывайте ключ пользователю полностью только один раз при создании. Дальше отображайте только «хвост» (например, последние 4 символа) и дату создания. Храните на сервере не сам ключ, а его хэш, чтобы даже при утечке базы ключи не уехали «как есть». Дайте ротацию, чтобы можно было быстро заменить ключ без переписки с поддержкой.
Сразу разделите ключи по назначению: «серверный ключ для продакшена» и «тестовый ключ». Это снижает шанс, что кто-то случайно удалит реальные данные, пока отлаживает интеграцию.
Не усложняйте разрешения, но и не оставляйте все как admin. На старте обычно хватает трех уровней: read, write, admin (для управления ключами и доступами).
Для предсказуемости интеграций ведите аудит. Минимум, который стоит логировать: кто вызвал API (идентификатор ключа или клиента), какой эндпоинт и метод, результат (код ответа) и время, важные изменения (что создали, обновили, удалили), откуда пришел запрос (IP или идентификатор приложения).
Если вы работаете в режиме «быстро собрать и проверить гипотезу», как это часто бывает на takprosto.ai, такая простая модель безопасности особенно спасает: понятные ключи, скоупы и тестовый режим экономят недели поддержки.
Хороший дизайн публичного API заметен не тогда, когда все работает, а когда что-то пошло не так. Клиенту важно быстро понять: это ошибка данных, прав, лимитов или временный сбой. И еще важнее - можно ли безопасно повторить запрос.
Договоритесь об одном JSON-формате для всех ошибок, чтобы клиент не угадывал, где искать причину. Минимальный набор: стабильный code, понятное message, опциональные details (например, по полям) и request_id для поддержки.
{
"error": {
"code": "validation_error",
"message": "Некорректные данные",
"details": {
"email": "Неверный формат",
"age": "Должно быть >= 18"
},
"request_id": "req_01HXYZ..."
}
}
request_id стоит возвращать и в заголовке, и в теле. Тогда партнер присылает один идентификатор, а вы быстро находите запрос в логах.
Лучше использовать небольшой, предсказуемый набор статусов:
200/201/204 для успеха (204 когда нет тела)400 для неверного запроса, 401 нет авторизации, 403 нет прав404 ресурс не найден, 409 конфликт (например, дубль уникального значения)422 удобно для ошибок валидации по полям (если вы готовы его поддерживать)429 лимиты, 500/503 ошибки сервисаЕсли сомневаетесь между 400 и 422, выберите одно правило и держитесь его везде.
Клиенты будут делать ретраи автоматически. Помогите им не создать дубли:
GET и DELETE (если удаление идемпотентно)POST повторять только с идемпотентным ключом или если вы явно это поддерживаете429 и 503 можно повторять с задержкой, 400/422 повторять бессмысленноЗолотое правило: новые поля в ответах добавлять можно, менять смысл старых нельзя. Клиент должен игнорировать неизвестные поля, а сервер не должен падать, если клиент прислал лишнее. Обычно лучше тихо игнорировать незнакомые поля и строго валидировать только то, что вы реально поддерживаете.
Простой сценарий: партнер парсит ответ и сохраняет только id и status. Через месяц вы добавили status_reason. Если клиент написан аккуратно, интеграция не ломается, а новые данные он сможет начать использовать позже.
Хорошая документация делает больше, чем половина «идеального» дизайн публичного API. Если человек может сделать первый запрос за 10 минут и понять ошибки без переписки с вами, интеграции начинают появляться сами.
Начните с короткого «быстрого старта». Не пишите длинные вводные, лучше покажите один рабочий пример запроса и ответа, а рядом объясните, где взять токен и какой базовый URL использовать.
Дальше описывайте каждый метод одинаковым шаблоном: что делает, какие параметры принимает, что вернет, какие ошибки возможны, и какие лимиты действуют. Примеры должны быть копируемыми и одинаковыми по стилю.
Обычно достаточно следующего набора: пример запроса (curl или один выбранный формат), пример успешного ответа, параметры и валидация (обязательный, тип, ограничения), список ошибок с понятными сообщениями, указание rate limit и правил пагинации.
Один справочник по ручкам не отвечает на вопрос «как решить мою задачу». Добавьте 2-3 коротких сценария, которые повторяют реальную интеграцию. Например: «создать объект, проверить статус, получить список и обновить». Это снижает число вопросов сильнее, чем дополнительные детали по каждому параметру.
Пример сценария: партнер получает заказы. Он создает доступ, делает запрос на список заказов с пагинацией, сохраняет cursor, затем раз в минуту догружает новые. Если получает 429, ждет время из заголовка и повторяет.
SDK на первом релизе должен быть тонкой оберткой над HTTP, а не отдельным «мини-продуктом». Достаточно одного клиента, конфига и единых ошибок.
Минимальная структура SDK:
Не спешите с генераторами, сложными типами, авто-рефрешем токенов и «умными» моделями. Пока у вас 10-20 методов, ручной SDK проще поддерживать и легче объяснить. Если вы выпускаете API для партнеров и хотите быстрый старт, простота важнее «умности».
У вас маленький SaaS для подписок. Первый партнер говорит: «Нам нужно раз в день выгружать платежи и сверять их со своей бухгалтерией». Он не просит «идеальный API на все случаи». Ему нужно надежно и предсказуемо забрать данные, не сломав ваш сервис.
Для v1 достаточно набора простых ресурсов вокруг платежей и клиентов. Например:
GET /v1/transactions (список)GET /v1/transactions/{id} (детали платежа)GET /v1/customers/{id} (кто платил, если нужно)GET /v1/balance или GET /v1/summary (итог за период, опционально)Ключевой эндпоинт - список транзакций. Сделайте пагинацию и фильтр по дате сразу, иначе партнер упрется в лимиты или начнет тянуть «все за год».
Хороший минимум: GET /v1/transactions?created_from=2026-01-01&created_to=2026-01-31&limit=100&cursor=.... В ответе верните items, next_cursor (или has_more) и зафиксируйте сортировку, например по created_at от новых к старым. Тогда интегратор понимает, как безопасно догружать следующую страницу.
Лимиты нужны, чтобы защититься от массовых выгрузок, но не заблокировать бизнес клиента. Дайте понятный коридор, например 60 запросов в минуту на API-ключ и отдельный лимит на тяжелый список транзакций. Если лимит превышен, отвечайте 429 и добавляйте Retry-After, чтобы партнер мог корректно повторить.
Документировать сразу стоит: формат дат, правила пагинации, примеры ответа, ошибки (включая 429), и как делать «ежедневную сверку» (рекомендуемый запрос по диапазону дат). На потом можно оставить расширенные фильтры, вебхуки, экспорты в файлы и сложные отчеты.
Перед релизом публичного API полезно пройтись по короткому списку и убедиться, что вы выпускаете не набор разрозненных ручек, а понятный контракт. Это экономит недели переписок с первым партнером и снижает риск поломок.
Проверьте эти пункты на реальных запросах, а не только «на бумаге»:
Сделайте 10-15 тестовых запросов руками: успешный кейс, неверный токен, отсутствие обязательного поля, конфликт (409), и самый «тяжелый» список. Отдельно проверьте большие выборки: 10, 100, 1000 элементов, а также ситуацию, когда данных нет. Если где-то поведение «плавает», лучше зафиксировать и упростить сейчас.
Типичные ошибки, которые быстро портят впечатление: менять названия полей без версии, скрывать лимиты (клиент не понимает, что происходит), не возвращать request_id (сложно искать проблему по логам), смешивать разные форматы пагинации в разных методах.
Сначала зафиксируйте контракт: схемы запросов и ответов, примеры, список кодов ошибок. Затем соберите минимальный набор эндпоинтов v1 и черновик SDK (например, один клиент с 3-5 методами и обработкой ошибок).
Если нужно выпустить быстрее, в TakProsto можно начать с planning mode: описать ресурсы и сценарии, получить каркас API и документации, а потом стабилизировать контракт. Когда все совпало с ожиданиями партнеров, разверните и хостите сервис, при необходимости экспортируйте исходники и подключите кастомный домен, сохраняя возможность отката через снапшоты.
Публичный API нужен, чтобы чужие системы могли стабильно работать с вашим сервисом без UI: интеграции с бухгалтерией, CRM/BI, партнёрские сценарии, ваши мобильные приложения и фоновые задачи.
Он ценен тем, что становится контрактом: предсказуемые ответы, ошибки и правила обновлений сильно уменьшают поддержку и повышают доверие интеграторов.
Для v1 выберите одну главную аудиторию (партнёры или клиенты, или внутренняя команда) и 2–3 сценария, которые реально будут использоваться каждую неделю.
Практичный старт — чтение данных:
idЗапись (create/update) добавляйте только если без неё сценарий не работает.
Держите границы жёстко: 5–10 ключевых операций и никаких «ещё одной маленькой ручки» без реального запроса.
Хороший ориентир:
Лучше выпустить маленький, но стабильный контракт, чем большой, который меняется каждую неделю.
Самый понятный старт — REST: ресурсы как существительные во множественном числе (/projects, /invoices) и объект как /projects/{id}.
По методам:
GET — чтениеPOST — создание или запуск операцииPATCH — частичное обновлениеDELETE — удаление/soft-deletePUT лучше не трогать в начале, если вы не готовы к «полной замене объекта» без сюрпризов.
Дефолт — JSON. Для ответов проще всего:
Например, для списков удобно стандартизировать:
items, next_cursor, has_morelimit, cursorТак вы сможете добавлять метаданные (лимиты, предупреждения) без ломки клиентов.
Сделайте cursor-пагинацию как основной вариант для SaaS: она обычно стабильнее, чем offset, когда данные меняются во время листания.
Минимальный контракт:
limit (с дефолтом) и опционально cursoritems, next_cursor, has_moreОбязательно зафиксируйте порядок сортировки (например, created_at desc + вторичный id), иначе страницы будут «прыгать».
Версионируйте только ломающее. Самый простой и понятный способ — версия в пути: /v1/....
Правила совместимости:
Планируя v2, заранее обозначьте срок поддержки v1 и период параллельной работы.
Дефолт — лимитировать по API-ключу: запросы в минуту/секунду + (опционально) лимит параллельных запросов и отдельные ограничения для тяжёлых эндпоинтов.
При превышении:
429 Too Many RequestsRetry-AfterКлиентам проще жить, когда лимиты не скрыты и поведение предсказуемо.
Для первого релиза обычно достаточно API key в заголовке Authorization: Bearer <key>.
Минимальные практики:
read, write, admin)OAuth2 имеет смысл позже, когда реально нужен доступ «от имени пользователя» и отзыв разрешений.
Сделайте единый JSON-формат ошибок и ограниченный, понятный набор HTTP-статусов.
Рекомендуемый минимум в ошибке:
code (стабильный)message (человеческий)details (ошибки по полям, если есть)request_id (для поддержки)По ретраям: безопасно повторять GET, а POST — только с идемпотентностью (например, Idempotency-Key), чтобы не создавать дубли.