6 мин

Проектирование публичного API для первых создателей SaaS: основы

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

Проектирование публичного API для первых создателей SaaS: основы

Настоящая проблема: выпустить API, которым можно поддерживать

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

Сложность не в том, чтобы написать v1. Сложность — удерживать его стабильным, пока вы фиксируете баги, добавляете фичи и узнаёте, чего на самом деле хотят клиенты.

Ранние решения проявятся позже в виде тикетов в поддержку. Если ответы меняют форму без предупреждения, если имена непоследовательны или клиенты не могут понять, успешно ли выполнился запрос, вы создаёте трения. Эти трения приводят к недоверию, а недоверие заставляет людей перестать строить на вашей платформе.

Скорость тоже важна. Большинству новичков в SaaS нужно быстро выпустить что-то полезное, а затем улучшать. Компромисс прост: чем быстрее вы выпускаете без правил, тем больше времени потратите на исправление этих решений, когда появятся реальные пользователи.

Для v1 «достаточно хорошо» обычно означает небольшой набор эндпоинтов, которые соответствуют реальным действиям пользователей, согласованную номенклатуру и форму ответов, понятную стратегию изменений (пусть даже просто «v1»), предсказуемую пагинацию и адекватные rate limits, а также документацию, показывающую точно, что отправлять и что вы получите в ответ.

Конкретный пример: представьте, что клиент делает интеграцию, которая создаёт счета каждую ночь. Если вы потом переименуете поле, смените формат дат или начнёте молча возвращать частичные результаты, их задача провалится в 2 часа ночи. Они обвинят ваш API, а не свой код.

Если вы строите с помощью чат-инструмента вроде Koder.ai, легко сгенерировать много эндпоинтов быстро. Это нормально, но держите публичную поверхность маленькой. Внутренние эндпоинты можно держать приватными, пока вы не поймёте, что должно войти в долгосрочный контракт.

Начните с небольшой, понятной поверхности API

Хороший публичный API начинается с выбора небольшого набора существительных (ресурсов), которые соответствуют тому, как клиенты говорят о вашем продукте. Держите имена ресурсов стабильными, даже если внутренняя база данных меняется. При добавлении функций предпочитайте добавлять поля или новые эндпоинты, а не переименовывать ключевые ресурсы.

Практический старт для многих SaaS-продуктов: users, organizations, projects и events. Если вы не можете объяснить ресурс в одном предложении — вероятно, он ещё не готов к публикации.

Держите использование HTTP скучным и предсказуемым:

  • GET читает данные (без побочных эффектов)
  • POST создаёт что‑то (или запускает действие)
  • PATCH обновляет несколько полей
  • DELETE удаляет или деактивирует

Аутентификация не обязана быть сложной на первом шаге. Если ваш API в основном «сервер‑to‑сервер» (клиенты вызывают его с бэкенда), API‑ключи часто достаточны. Если клиентам нужно действовать от лица отдельных пользователей или вы ожидаете интеграции, где пользователи дают доступ, OAuth обычно более подходящ. Запишите решение простым языком: кто вызывает, и к чьим данным у них есть доступ?

Установите ожидания заранее. Ясно укажите, что поддерживается, а что — поведение по возможности. Например: list‑эндпоинты стабильны и обратно совместимы, но фильтры поиска могут расширяться и не гарантируются как исчерпывающие. Это снижает тикеты в поддержку и даёт вам свободу улучшать.

Если вы используете платформу вроде Koder.ai, рассматривайте API как продуктовый контракт: сначала маленький контракт, затем расширяйте его на основе реального использования, а не домыслов.

Версионирование без загнания себя в угол

Версионирование — это, в первую очередь, про ожидания. Клиенты хотят знать: сломается ли моя интеграция на следующей неделе? Вы хотите место для улучшений без страха.

Версионирование в URL против заголовков

Версионирование в заголовках может выглядеть аккуратно, но его легко скрыть от логов, кэшей и скриншотов для поддержки. Версионирование в URL обычно проще: /v1/.... Когда клиент присылает падающий запрос, версию видно сразу. Также легко держать v1 и v2 рядом.

Что такое ломающее изменение на самом деле?

Изменение ломающее, если поведение корректного клиента может перестать работать без изменения его кода. Частые примеры:

  • Переименование поля (например customer_idcustomerId)
  • Смена типа поля (строка → число) или его смысла
  • Удаление эндпоинта или поля ответа, на которое могли опираться клиенты
  • Ужесточение правил валидации (было опционально, стало обязательным)
  • Изменение требований к аутентификации или прав по умолчанию

Безопасное изменение — такое, которое старые клиенты могут игнорировать. Добавление нового опционального поля обычно безопасно. Например, добавление plan_name в ответ GET /v1/subscriptions не сломает клиентов, которые читают только status.

Практическое правило: не удаляйте и не переопределяйте поля внутри одного мажорного релиза. Добавляйте новые поля, сохраняйте старые и удаляйте их только при деактивации всей версии.

Политика устаревания, которой можно следовать

Держите всё просто: объявляйте устаревание заранее, возвращайте понятное предупреждение в ответах и указывайте конечную дату. Для первого API окно в 90 дней часто реалистично. В течение этого времени поддерживайте v1, опубликуйте короткую заметку о миграции и убедитесь, что поддержка может сослаться на одно предложение: v1 работает до этой даты; вот что изменилось в v2.

Если вы строите на платформе вроде Koder.ai, относитесь к версиям как к снимкам: выпускайте улучшения в новой версии, держите старую стабильной и отключайте её только после того, как клиенты успеют перейти.

Паттерны пагинации, которые остаются предсказуемыми

Пагинация — место, где выигрывается или теряется доверие. Если результаты меняются между запросами, люди перестают доверять API.

Используйте page/limit, когда набор данных невелик, запрос прост и пользователи часто хотят, например, третью страницу из 20. Используйте курсорную пагинацию, когда списки растут, новые элементы приходят часто или пользователь много сортирует и фильтрует. Курсор сохраняет последовательность даже при добавлении новых записей.

Несколько правил для надёжной пагинации:

  • Всегда задавайте сортировку по умолчанию (например created_at desc).
  • Добавляйте разрыв‑ключ (например id), чтобы порядок был детерминирован.
  • Рассматривайте пагинацию как часть контракта: изменение сортировки позже — ломающее изменение.
  • Возвращайте минимум, необходимый для продолжения: items плюс следующий курсор (или следующая страница).

Число 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=="
}

Ограничения по частоте и вежливые повторы

Version Without Breaking Users
Spin up v2 safely while keeping v1 stable, then migrate clients on your timeline.

Rate limits — это не столько про строгость, сколько про доступность. Они защищают ваше приложение от всплесков трафика, базу данных от дорогих запросов и ваш бюджет от неожиданных счетов за инфраструктуру. Лимит — это ещё и контракт: клиенты знают, какой трафик считается нормальным.

Начните просто и настраивайте позже. Выберите значение, покрывающее типичное использование с запасом на короткие всплески, затем смотрите на реальный трафик. Если данных нет, безопасный дефолт — лимит на API‑ключ примерно 60 запросов в минуту плюс небольшая возможность всплеска. Если один эндпоинт существенно тяжелее (поиск, экспорт) — задайте строже его лимит или отдельную «стоимость», вместо того чтобы наказывать все запросы.

Когда вы вводите лимиты, облегчите клиентам правильное поведение. Возвращайте 429 Too Many Requests и включайте несколько стандартных заголовков:

  • X-RateLimit-Limit: максимум в окне
  • X-RateLimit-Remaining: сколько осталось
  • X-RateLimit-Reset: когда окно сбрасывается (timestamp или секунды)
  • Retry-After: сколько ждать перед повтором

Клиенты должны воспринимать 429 как нормальное состояние, а не как ошибку, которую нужно обходить. Вежливый паттерн повтора делает обе стороны счастливее:

  • Ждите Retry-After, если он есть
  • Иначе используйте экспоненциальный бэкофф (например 1s, 2s, 4s)
  • Добавляйте небольшую случайность (jitter), чтобы множество клиентов не повторяли одновременно
  • Ограничьте максимальное время ожидания (например 30–60s)

Пример: если клиент запускает 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‑эндпоинтах.
  • Сохраняйте ключ вместе с результатом на короткое окно (например 24 часа).
  • Для повторного запроса с тем же ключом возвращайте тот же ответ, что и для первого запроса.
  • Если первый запрос всё ещё выполняется, возвращайте ясный ответ «повторите попытку», вместо создания второго ресурса.

Один такой заголовок предотвращает много неприятных краёвых случаев, когда сеть нестабильна или клиенты получают таймауты.

Пример: небольшой SaaS‑billing API на практике

Представьте простой 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 Requests
  • Retry-After: 20
  • небольшое тело ошибки вроде { "error": { "code": "rate_limited" } }

На стороне клиента задача может подождать 20 секунд, затем продолжить с того же cursor, не перекачивая всё заново и не создавая дубликаты инвойсов.

Пошагово: простой план релиза для вашей v1 API

Set Friendly Rate Limits
Add rate limits and 429 handling with clear headers so clients retry politely.

Запуск v1 проходит лучше, если вы относитесь к нему как к небольшому продуктовому релизу, а не к набору эндпоинтов. Цель проста: люди могут на этом строить, а вы можете улучшать без сюрпризов.

Практический план на 1 неделю (даже для маленькой команды)

Начните с одной страницы, объясняющей, для чего ваш API и для чего он не предназначен. Держите поверхность достаточно маленькой, чтобы объяснить вслух за минуту.

Следуйте этой последовательности и не переходите дальше, пока каждый шаг не станет «достаточно хорошим»:

  1. Набросайте одностраничную спецификацию: назовите основные ресурсы и первые эндпоинты (5–10). Включите базовый URL, метод аутентификации и требуемые заголовки.
  2. Для каждого эндпоинта напишите один реалистичный запрос и один реалистичный ответ. Используйте реальные имена полей — эти примеры станут первыми доками и первыми тестами.
  3. Добавьте короткий раздел правил: как выглядят ошибки, как работает пагинация (если есть list), какие лимиты и какие поля стабильны или могут меняться.
  4. Проведите внутренний тест с фейковым клиентом. Притворитесь, что вы клиент, который делает интеграцию в новом репозитории. Засеките время до первого успешного вызова и отметьте, где возникали вопросы.
  5. Опубликуйте контракт v1: какие изменения безопасны (добавление полей), какие требуют новой версии и сколько уведомления вы даёте о ломающих изменениях.

Если вы используете workflow с генерацией кода (например, Koder.ai для scaffold), всё равно сделайте тест с фейковым клиентом. Сгенерированный код может выглядеть корректно, но быть неудобным в использовании.

Выигрыш: меньше писем в поддержку, меньше хотфиксов и v1, который вы реально сможете поддерживать.

Выпуск небольшого SDK без оверхедов

Первый SDK — это не второй продукт. Думайте о нём как о тонкой, удобной оболочке над HTTP API. Он должен облегчать распространённые вызовы, но не скрывать, как работает API. Если нужна фича, которой пока нет в обёртке, клиент должен иметь возможность отправить «сырые» запросы.

Выберите один язык для старта, исходя из того, что реально используют ваши клиенты. Для многих B2B SaaS это JavaScript/TypeScript или Python. Один качественный SDK лучше, чем три наполовину готовых.

Что должно включать минимальное SDK

Хороший стартовый набор:

  • Централизованная обработка аутентификации (API‑ключ или OAuth токен)
  • Разумные таймауты и авто‑повторы для безопасных запросов (GET) с бэкоффом
  • Хелперы по пагинации, возвращающие итератор или функцию next page
  • Понятные модели запросов/ответов (даже простые типы)
  • «Запасной выход» для кастомных заголовков и сырых HTTP‑вызовов

Можно писать вручную или генерировать из OpenAPI. Генерация хороша, когда спецификация точная и вы хотите типизацию, но часто даёт много кода. Ранним этапом достаточно вручную написанного минимального клиента плюс OpenAPI для доков. Позже можно перейти к сгенерированным клиентам, не ломая пользователей, если публичный интерфейс SDK останется стабильным.

Версионируйте SDK отдельно от API

Версия API должна следовать правилам совместимости. Версия SDK — правилам упаковки.

Если вы добавили новые опциональные параметры или эндпоинты — это обычно минорный апдейт SDK. Делайте мажорные релизы SDK для ломающих изменений в самом SDK (переименованные методы, изменённые дефолты), даже если API остался прежним. Это снижает хаос при апгрейдах и количество тикетов.

Частые ошибки, которые создают головную боль в поддержке

Prevent Duplicate Writes
Implement Idempotency-Key handling with generated server code you can edit and own.

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

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

Пагинация — ещё один частый источник проблем. Симптомы: один эндпоинт использует page/pageSize, другой — offset/limit, третий — cursors, и у всех разные дефолты. Выберите один паттерн для v1 и используйте везде. Держите сортировку стабильной, чтобы следующая страница не пропускала и не дублировала элементы при добавлении новых записей.

Ошибки создают много переписок, когда формат непоследовательный. Частая ошибка — один сервис возвращает { "error":"..." }, другой — { "message":"..." } с разными кодами статусов за одно и то же. Клиенты начинают писать грязные, endpoint‑специфичные обработчики.

Пять ошибок, которые порождают самые длинные письма:

  • Молчаливые изменения полей (имена, типы, смысл) без bump‑а версии или окна депрекации
  • Правила пагинации, которые отличаются по эндпоинтам или меняют дефолты со временем
  • Разные форматы ошибок, коды статусов или сообщения валидации по разным эндпоинтам
  • Отсутствие request_id, чтобы быстро найти запрос в логах
  • Rate limits, которые срабатывают внезапно, без заголовков, ясных инструкций по повтору и примера бэкоффа

Простая привычка помогает: в каждом ответе включайте request_id, а в каждом 429 объясняйте, когда пробовать снова.

Быстрый чеклист и дальнейшие шаги

Прежде чем публиковать, пройдитесь финально по консистентности. Большинство тикетов возникает из‑за мелких несоответствий между эндпоинтами, доками и примерами.

Быстрые проверки, ловящие большинство проблем:

  • Именование: согласованные существительные, множественные ресурсы и стиль написания полей (выберите один и придерживайтесь).
  • Примеры: у каждого эндпоинта есть реалистичный запрос и ответ, включая пагинацию.
  • Ошибки: единый формат ошибки, стабильные коды ошибок и полезные сообщения для частых сбоев.
  • Лимиты: поведение rate limit документировано, ответы содержат ожидаемые заголовки.
  • Безопасность: идемпотентность для повторов при создании и предсказуемая обработка таймаутов.

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

Следите за этими сигналами в первую очередь:

  • Всплески 429 (кто ограничивается и почему)
  • p95 latency по эндпоинтам (медленные эндпоинты часто скрывают N+1 запросы)
  • Топ эндпоинтов и параметров (реальная поверхность API)
  • Уровень ошибок по кодам статусов (400 vs 401 vs 500)
  • Таймауты и повторы (клиенты могут зациклиться незаметно)

Собирайте фидбек, не переписывая всё. Добавьте в доки путь для отчётов и помечайте каждый репорт эндпоинтом, request id и версией клиента. Когда вы что‑то фиксите, предпочитайте аддитивные изменения: новые поля, опциональные параметры или новый эндпоинт, вместо ломки существующего поведения.

Дальше: напишите одностраничную спецификацию с ресурсами, планом версионирования, правилами пагинации и форматом ошибок. Затем подготовьте доки и крошечный starter SDK с аутентификацией и 2–3 ключевыми эндпоинтами. Если хотите двигаться быстрее, можно набросать спецификацию, доки и SDK из чат‑плана с помощью инструментов вроде Koder.ai (режим планирования помогает связать эндпоинты и примеры перед генерацией кода).

FAQ

How many endpoints should I expose in my first public API?

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.

What resources should I start with for a SaaS API?

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.

Which HTTP methods should I use, and why does it matter?

Use the standard meanings and be consistent:

  • GET = read (no side effects)
  • POST = create or start an action
  • PATCH = partial update
  • DELETE = remove or disable

The main win is predictability: clients shouldn’t guess what a method does.

Should I version my API in the URL or in headers?

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.

What counts as a breaking change in an API?

A change is breaking if a correct client can fail without changing their code. Common examples:

  • Renaming fields
  • Changing field types or meaning
  • Removing fields or endpoints
  • Making previously-optional input required
  • Changing auth rules or default permissions

Adding a new optional field is usually safe.

What’s a realistic deprecation policy for a v1 API?

Keep it simple:

  • Announce it early
  • Include a clear warning in responses (and/or docs)
  • Set a firm end date

A practical default is a 90-day window for a first API, so customers have time to migrate without panic.

Should I use page-based or cursor-based pagination?

Pick one pattern and stick to it across all list endpoints.

  • Use page/limit when datasets are small and people jump to page 3.
  • Use cursor when lists grow large or change often.

Always define a default sort and a tie-breaker (like created_at + id) so results don’t jump around.

What rate limit should I set, and what should I return on 429?

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-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
  • Retry-After

This makes retries predictable and reduces support tickets.

What should a good API error response look like?

Use 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.

How do I ship docs and a small SDK without over-engineering it?

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:

  • Write one realistic request + response example per endpoint
  • Lock in pagination and error shapes
  • Add idempotency keys for important POST actions
  • Build a “fake client” internally to see where it’s confusing

Then publish a tiny SDK that helps with auth, timeouts, retries for safe requests, and pagination—without hiding how the HTTP API works.

Похожие статьи