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

Публичный API — это не просто конечная точка, которую выставляет ваше приложение. Это обещание людям вне вашей команды, что контракт будет работать и дальше, даже когда вы меняете продукт.
Сложность не в том, чтобы написать v1. Сложность — удерживать его стабильным, пока вы фиксируете баги, добавляете фичи и узнаёте, чего на самом деле хотят клиенты.
Ранние решения проявятся позже в виде тикетов в поддержку. Если ответы меняют форму без предупреждения, если имена непоследовательны или клиенты не могут понять, успешно ли выполнился запрос, вы создаёте трения. Эти трения приводят к недоверию, а недоверие заставляет людей перестать строить на вашей платформе.
Скорость тоже важна. Большинству новичков в SaaS нужно быстро выпустить что-то полезное, а затем улучшать. Компромисс прост: чем быстрее вы выпускаете без правил, тем больше времени потратите на исправление этих решений, когда появятся реальные пользователи.
Для v1 «достаточно хорошо» обычно означает небольшой набор эндпоинтов, которые соответствуют реальным действиям пользователей, согласованную номенклатуру и форму ответов, понятную стратегию изменений (пусть даже просто «v1»), предсказуемую пагинацию и адекватные rate limits, а также документацию, показывающую точно, что отправлять и что вы получите в ответ.
Конкретный пример: представьте, что клиент делает интеграцию, которая создаёт счета каждую ночь. Если вы потом переименуете поле, смените формат дат или начнёте молча возвращать частичные результаты, их задача провалится в 2 часа ночи. Они обвинят ваш API, а не свой код.
Если вы строите с помощью чат-инструмента вроде Koder.ai, легко сгенерировать много эндпоинтов быстро. Это нормально, но держите публичную поверхность маленькой. Внутренние эндпоинты можно держать приватными, пока вы не поймёте, что должно войти в долгосрочный контракт.
Хороший публичный API начинается с выбора небольшого набора существительных (ресурсов), которые соответствуют тому, как клиенты говорят о вашем продукте. Держите имена ресурсов стабильными, даже если внутренняя база данных меняется. При добавлении функций предпочитайте добавлять поля или новые эндпоинты, а не переименовывать ключевые ресурсы.
Практический старт для многих SaaS-продуктов: users, organizations, projects и events. Если вы не можете объяснить ресурс в одном предложении — вероятно, он ещё не готов к публикации.
Держите использование HTTP скучным и предсказуемым:
GET читает данные (без побочных эффектов)POST создаёт что‑то (или запускает действие)PATCH обновляет несколько полейDELETE удаляет или деактивируетАутентификация не обязана быть сложной на первом шаге. Если ваш API в основном «сервер‑to‑сервер» (клиенты вызывают его с бэкенда), API‑ключи часто достаточны. Если клиентам нужно действовать от лица отдельных пользователей или вы ожидаете интеграции, где пользователи дают доступ, OAuth обычно более подходящ. Запишите решение простым языком: кто вызывает, и к чьим данным у них есть доступ?
Установите ожидания заранее. Ясно укажите, что поддерживается, а что — поведение по возможности. Например: list‑эндпоинты стабильны и обратно совместимы, но фильтры поиска могут расширяться и не гарантируются как исчерпывающие. Это снижает тикеты в поддержку и даёт вам свободу улучшать.
Если вы используете платформу вроде Koder.ai, рассматривайте API как продуктовый контракт: сначала маленький контракт, затем расширяйте его на основе реального использования, а не домыслов.
Версионирование — это, в первую очередь, про ожидания. Клиенты хотят знать: сломается ли моя интеграция на следующей неделе? Вы хотите место для улучшений без страха.
Версионирование в заголовках может выглядеть аккуратно, но его легко скрыть от логов, кэшей и скриншотов для поддержки. Версионирование в URL обычно проще: /v1/.... Когда клиент присылает падающий запрос, версию видно сразу. Также легко держать v1 и v2 рядом.
Изменение ломающее, если поведение корректного клиента может перестать работать без изменения его кода. Частые примеры:
customer_id → customerId)Безопасное изменение — такое, которое старые клиенты могут игнорировать. Добавление нового опционального поля обычно безопасно. Например, добавление plan_name в ответ GET /v1/subscriptions не сломает клиентов, которые читают только status.
Практическое правило: не удаляйте и не переопределяйте поля внутри одного мажорного релиза. Добавляйте новые поля, сохраняйте старые и удаляйте их только при деактивации всей версии.
Держите всё просто: объявляйте устаревание заранее, возвращайте понятное предупреждение в ответах и указывайте конечную дату. Для первого API окно в 90 дней часто реалистично. В течение этого времени поддерживайте v1, опубликуйте короткую заметку о миграции и убедитесь, что поддержка может сослаться на одно предложение: v1 работает до этой даты; вот что изменилось в v2.
Если вы строите на платформе вроде Koder.ai, относитесь к версиям как к снимкам: выпускайте улучшения в новой версии, держите старую стабильной и отключайте её только после того, как клиенты успеют перейти.
Пагинация — место, где выигрывается или теряется доверие. Если результаты меняются между запросами, люди перестают доверять API.
Используйте page/limit, когда набор данных невелик, запрос прост и пользователи часто хотят, например, третью страницу из 20. Используйте курсорную пагинацию, когда списки растут, новые элементы приходят часто или пользователь много сортирует и фильтрует. Курсор сохраняет последовательность даже при добавлении новых записей.
Несколько правил для надёжной пагинации:
created_at desc).id), чтобы порядок был детерминирован.Число total_count — это сложно. total_count может быть дорогим на больших таблицах, особенно с фильтрами. Если вы можете предоставить его дешево — включите. Если нет — опустите или сделайте опциональным через флаг запроса.
Here are simple request/response shapes.
// Page/limit
GET /v1/invoices?page=2&limit=25&sort=created_at_desc
{
"items": [{"id":"inv_1"},{"id":"inv_2"}],
"page": 2,
"limit": 25,
"total_count": 142
}
// Cursor-based
GET /v1/invoices?limit=25&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDozMDowMFoiLCJpZCI6Imludl8xMDAifQ==
{
"items": [{"id":"inv_101"},{"id":"inv_102"}],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDoyNTowMFoiLCJpZCI6Imludl8xMjUifQ=="
}
Rate limits — это не столько про строгость, сколько про доступность. Они защищают ваше приложение от всплесков трафика, базу данных от дорогих запросов и ваш бюджет от неожиданных счетов за инфраструктуру. Лимит — это ещё и контракт: клиенты знают, какой трафик считается нормальным.
Начните просто и настраивайте позже. Выберите значение, покрывающее типичное использование с запасом на короткие всплески, затем смотрите на реальный трафик. Если данных нет, безопасный дефолт — лимит на API‑ключ примерно 60 запросов в минуту плюс небольшая возможность всплеска. Если один эндпоинт существенно тяжелее (поиск, экспорт) — задайте строже его лимит или отдельную «стоимость», вместо того чтобы наказывать все запросы.
Когда вы вводите лимиты, облегчите клиентам правильное поведение. Возвращайте 429 Too Many Requests и включайте несколько стандартных заголовков:
X-RateLimit-Limit: максимум в окнеX-RateLimit-Remaining: сколько осталосьX-RateLimit-Reset: когда окно сбрасывается (timestamp или секунды)Retry-After: сколько ждать перед повторомКлиенты должны воспринимать 429 как нормальное состояние, а не как ошибку, которую нужно обходить. Вежливый паттерн повтора делает обе стороны счастливее:
Retry-After, если он естьПример: если клиент запускает nightly sync, который сильно бьёт по вашему API, их задача может распределять запросы по минуте и автоматически замедляться на 429, вместо того чтобы вся задача падала.
Если ошибки вашего API трудно читать, тикетов в поддержку быстро накопится много. Выберите один формат ошибки и придерживайтесь его везде, включая 500. Простая и понятная схема: code, message, details и request_id, который пользователь может вставить в чат поддержки.
Вот небольшой предсказуемый формат:
{
"error": {
"code": "validation_error",
"message": "Some fields are invalid.",
"details": {
"fields": [
{"name": "email", "issue": "must be a valid email"},
{"name": "plan", "issue": "must be one of: free, pro, business"}
]
},
"request_id": "req_01HT..."
}
}
Используйте HTTP‑коды статусов одинаково всегда: 400 для неверного ввода, 401 когда auth отсутствует или невалиден, 403 когда пользователь аутентифицирован, но не имеет прав, 404 когда ресурс не найден, 409 для конфликтов (дублирование уникального значения или неверное состояние), 429 для rate limits и 500 для ошибок сервера. Последовательность важнее остроумия.
Делайте ошибки валидации простыми для исправления. Подсказки по полям должны указывать точное имя параметра, которое есть в документации, а не внутренний столбец БД. Если есть требование по формату (дата, валюта, enum) — указывайте, что вы принимаете и показывайте пример.
Повторы — место, где многие API случайно создают дубли. Для важных POST‑действий (платежи, создание счётов, отправка писем) поддерживайте идемпотентные ключи, чтобы клиенты могли безопасно повторять запросы.
Idempotency-Key на выбранных POST‑эндпоинтах.Один такой заголовок предотвращает много неприятных краёвых случаев, когда сеть нестабильна или клиенты получают таймауты.
Представьте простой SaaS с тремя основными объектами: projects, users и invoices. У проекта много пользователей, и каждый проект получает ежемесячные счета. Клиенты хотят синхронизировать счета в своей бухгалтерии и показывать базовую биллинг‑информацию в своём приложении.
Чистая v1 может выглядеть так:
GET /v1/projects/{project_id}
GET /v1/projects/{project_id}/invoices
POST /v1/projects/{project_id}/invoices
Теперь случилось ломающее изменение. В v1 суммы счётов хранились в целых центах: amount_cents: 1299. Позже вам нужны мультивалютность и десятичные значения, и вы хотите amount: "12.99" и currency: "USD". Если перезаписать старое поле, все существующие интеграции сломаются. Версионирование снимает панику: держите v1 стабильным, выпустите /v2/... с новыми полями и поддерживайте обе версии, пока клиенты не мигрируют.
Для листинга инвойсов используйте предсказуемую форму пагинации. Например:
GET /v1/projects/p_123/invoices?limit=50&cursor=eyJpZCI6Imludl85OTkifQ==
200 OK
{
"data": [ {"id":"inv_1001"}, {"id":"inv_1000"} ],
"next_cursor": "eyJpZCI6Imludl8xMDAwIn0="
}
Однажды клиент импортирует инвойсы в цикле и попадает в ваш rate limit. Вместо случайных падений он получает понятный ответ:
429 Too Many RequestsRetry-After: 20{ "error": { "code": "rate_limited" } }На стороне клиента задача может подождать 20 секунд, затем продолжить с того же cursor, не перекачивая всё заново и не создавая дубликаты инвойсов.
Запуск v1 проходит лучше, если вы относитесь к нему как к небольшому продуктовому релизу, а не к набору эндпоинтов. Цель проста: люди могут на этом строить, а вы можете улучшать без сюрпризов.
Начните с одной страницы, объясняющей, для чего ваш API и для чего он не предназначен. Держите поверхность достаточно маленькой, чтобы объяснить вслух за минуту.
Следуйте этой последовательности и не переходите дальше, пока каждый шаг не станет «достаточно хорошим»:
Если вы используете workflow с генерацией кода (например, Koder.ai для scaffold), всё равно сделайте тест с фейковым клиентом. Сгенерированный код может выглядеть корректно, но быть неудобным в использовании.
Выигрыш: меньше писем в поддержку, меньше хотфиксов и v1, который вы реально сможете поддерживать.
Первый SDK — это не второй продукт. Думайте о нём как о тонкой, удобной оболочке над HTTP API. Он должен облегчать распространённые вызовы, но не скрывать, как работает API. Если нужна фича, которой пока нет в обёртке, клиент должен иметь возможность отправить «сырые» запросы.
Выберите один язык для старта, исходя из того, что реально используют ваши клиенты. Для многих B2B SaaS это JavaScript/TypeScript или Python. Один качественный SDK лучше, чем три наполовину готовых.
Хороший стартовый набор:
Можно писать вручную или генерировать из OpenAPI. Генерация хороша, когда спецификация точная и вы хотите типизацию, но часто даёт много кода. Ранним этапом достаточно вручную написанного минимального клиента плюс OpenAPI для доков. Позже можно перейти к сгенерированным клиентам, не ломая пользователей, если публичный интерфейс SDK останется стабильным.
Версия API должна следовать правилам совместимости. Версия SDK — правилам упаковки.
Если вы добавили новые опциональные параметры или эндпоинты — это обычно минорный апдейт SDK. Делайте мажорные релизы SDK для ломающих изменений в самом SDK (переименованные методы, изменённые дефолты), даже если API остался прежним. Это снижает хаос при апгрейдах и количество тикетов.
Большинство тикетов по API — не про баги, а про сюрпризы. Проектирование публичного API — это в основном быть скучным и предсказуемым, чтобы клиентский код работал месяц за месяцем.
Самый быстрый способ потерять доверие — менять ответы без уведомления. Переименовали поле, сменили тип или начали возвращать null вместо значения — вы сломаете клиентов так, что им трудно будет диагностировать проблему. Если поведение действительно нужно поменять, сделайте версию или добавьте новое поле и некоторое время держите старое с ясным планом удаления.
Пагинация — ещё один частый источник проблем. Симптомы: один эндпоинт использует page/pageSize, другой — offset/limit, третий — cursors, и у всех разные дефолты. Выберите один паттерн для v1 и используйте везде. Держите сортировку стабильной, чтобы следующая страница не пропускала и не дублировала элементы при добавлении новых записей.
Ошибки создают много переписок, когда формат непоследовательный. Частая ошибка — один сервис возвращает { "error":"..." }, другой — { "message":"..." } с разными кодами статусов за одно и то же. Клиенты начинают писать грязные, endpoint‑специфичные обработчики.
Пять ошибок, которые порождают самые длинные письма:
Простая привычка помогает: в каждом ответе включайте request_id, а в каждом 429 объясняйте, когда пробовать снова.
Прежде чем публиковать, пройдитесь финально по консистентности. Большинство тикетов возникает из‑за мелких несоответствий между эндпоинтами, доками и примерами.
Быстрые проверки, ловящие большинство проблем:
После релиза смотрите за тем, что люди реально вызывают, а не за тем, как вы надеялись, что будут использовать API. Для начала достаточно небольшой панели и еженедельного обзора.
Следите за этими сигналами в первую очередь:
Собирайте фидбек, не переписывая всё. Добавьте в доки путь для отчётов и помечайте каждый репорт эндпоинтом, request id и версией клиента. Когда вы что‑то фиксите, предпочитайте аддитивные изменения: новые поля, опциональные параметры или новый эндпоинт, вместо ломки существующего поведения.
Дальше: напишите одностраничную спецификацию с ресурсами, планом версионирования, правилами пагинации и форматом ошибок. Затем подготовьте доки и крошечный starter SDK с аутентификацией и 2–3 ключевыми эндпоинтами. Если хотите двигаться быстрее, можно набросать спецификацию, доки и SDK из чат‑плана с помощью инструментов вроде Koder.ai (режим планирования помогает связать эндпоинты и примеры перед генерацией кода).
Start with 5–10 endpoints that map to real customer actions.
A good rule: if you can’t explain a resource in one sentence (what it is, who owns it, how it’s used), keep it private until you learn more from usage.
Pick a small set of stable nouns (resources) customers already use in conversation, and keep those names stable even if your database changes.
Common starters for SaaS are users, organizations, projects, and events—then add more only when there’s clear demand.
Use the standard meanings and be consistent:
GET = read (no side effects)POST = create or start an actionPATCH = partial updateDELETE = remove or disableThe main win is predictability: clients shouldn’t guess what a method does.
Default to URL versioning like /v1/....
It’s easier to see in logs and screenshots, easier to debug with customers, and simpler to run v1 and v2 side by side when you need a breaking change.
A change is breaking if a correct client can fail without changing their code. Common examples:
Adding a new optional field is usually safe.
Keep it simple:
A practical default is a 90-day window for a first API, so customers have time to migrate without panic.
Pick one pattern and stick to it across all list endpoints.
Always define a default sort and a tie-breaker (like created_at + ) so results don’t jump around.
Start with a clear per-key limit (for example 60 requests/minute plus a small burst), then adjust based on real traffic.
When limiting, return 429 and include:
X-RateLimit-LimitX-RateLimit-RemainingUse one error format everywhere (including 500s). A practical shape is:
code (stable identifier)message (human-readable)details (field-level issues)request_id (for support)Also keep status codes consistent (400/401/403/404/409/429/500) so clients can handle errors cleanly.
If you generate lots of endpoints quickly (for example with Koder.ai), keep the public surface small and treat it as a long-term contract.
Do this before launch:
POST actionsThen publish a tiny SDK that helps with auth, timeouts, retries for safe requests, and pagination—without hiding how the HTTP API works.
idX-RateLimit-ResetRetry-AfterThis makes retries predictable and reduces support tickets.