Попробовать API
</>
Генерация SDK из OpenAPI, white-label и open-source
platformaКейс
12 мин

Генерация SDK из OpenAPI, white-label и open-source

TL;DR: год назад был внутренний API «для своих», сегодня — публичная спецификация на OpenAPI, три SDK на разных языках, white-label для интеграторов и четыре репозитория на GitHub. Путь не прямой: генератор переписывали дважды, партнёрский слой сначала навесили флагами, а open-source чуть не закопали. Ниже — что работает, какой код за этим стоит и что сделали бы иначе.

Коротко

  • Ручной SDK — техдолг с первого дня. Он ломается тремя способами (documentation / type / coverage drift), и все три неизбежны. Лечение — одна openapi.yaml как точка правды и openapi-generator-cli: один файл → три SDK + Redoc + Swagger UI.
  • White-label — не продукт поверх API, а набор архитектурных свойств. Отдельный тип ключа, sub-проекты, жёсткая изоляция WHERE project_id = ? и отдельный префикс /partner/v1/* вместо флагов поверх обычных эндпоинтов.
  • Open-source SDK — не подарок сообществу, а снятие якоря. Закрытая библиотека от внешнего вендора — первый кандидат на блок у корпоративной security. Открытый код под MIT убирает этот блок и работает как сигнал зрелости.

Точка А: API «для своих»

Был внутренний API. Под «своими» — наша же админка, кабинет клиента, пара служебных скриптов. Никакой публичности, никаких SDK. Документация жила в docs/INTERNAL_API.md, машинно-читаемая спецификация отсутствовала как сущность. Каждый и так знал, что POST /contacts принимает phone и name; если забывал — открывал контроллер.

В таком состоянии можно жить, пока ты единственный потребитель API. Как только начинается публичный доступ, модель ломается: потребитель на другом языке не может «открыть контроллер», ему нужен контракт. Именно с этого места начинается вся история — три тезиса ниже про то, как контракт превратился в платформу.

Ручные SDK: три вида drift

Первая мысль при выходе на тех-аудиторию: напишем три SDK руками. Один пишет PHP, другой Python, третий Node. За месяц закроем. Так и сделали. Получилось красиво — у каждого SDK свой характер: PHP по-laravel, Python pythonic, Node с async/await повсюду.

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

  • Documentation drift. API получил новый endpoint. Через неделю реализовали в PHP-SDK, через две — в Python, через три — в Node. В этот промежуток клиенты на разных языках видят разный набор возможностей. На странице документации метод уже описан, а в библиотеке его ещё нет — и прилетает тикет «у вас в доках есть метод X, а в SDK его нет».
  • Type drift. Сервер отдаёт created_at строкой ISO 8601. В PHP-SDK его типизировали как string, в Python — как datetime, в Node — как Date | string. Меняем формат на сервере — это один коммит; в трёх SDK это три pull request, три релиза, три обновления доки, и вероятность, что все три пройдут синхронно, стремится к нулю. client.contacts.create() в Python принимает phone и name, в PHP — массив, в Node — объект с camelCase. Разработчик, переключающийся между языками, вынужден переучиваться.
  • Coverage drift. Из 60 эндпоинтов PHP покрыл 60, Python — 55, Node — 47. Появляется метод № 61 — у него три разных приоритета в трёх командах. Поддерживать паритет ручным трудом возможно только когда есть отдельный «синхронизатор», единственная задача которого — следить за паритетом. У нас такого человека не было.

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

OpenAPI как единственная точка правды

Сели и описали API в OpenAPI 3.1. Файл openapi.yaml стал точкой правды: он описывает каждый endpoint, каждую модель, каждый код ошибки, каждое поле запроса и ответа. Из него генерируется всё остальное.

Цикл выпуска новой версии теперь выглядит так:

  1. Правка в openapi.yaml (новый endpoint, новое поле, новый код ошибки).
  2. openapi-generator-cli с пресетом для PHP → обновлённый sdk-php.
  3. То же для Python → sdk-python.
  4. То же для Node → sdk-node.
  5. Из той же openapi.yaml рендерится Redoc — публичная документация на developers.botix.pro.
  6. Из той же openapi.yaml рендерится Swagger UI на developers.botix.pro/try-it — интерактивная песочница.

Один файл — шесть артефактов. Любое изменение распространяется через генератор за минуту, без копипасты. Про то, как из той же спеки держать zero-dependency SDK с одинаковой сигнатурой на всех языках, — в разборе Zero-dependency SDK, open-source и партнёрский API.

Отдельная ценность — код-сэмплы прямо в спеке. Обычно примеры пишутся вручную в Markdown, и их синхронизация с API — отдельная боль. В OpenAPI 3 есть расширение x-codeSamples — примеры хранятся под каждым endpoint:

yaml
paths:
  /contacts:
    post:
      x-codeSamples:
        - lang: curl
          source: |
            curl -X POST https://api.botix.pro/public/v1/contacts \
              -H "Authorization: Bearer $BOTIX_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"phone":"+79991234567","name":"Test"}'
        - lang: PHP
          source: |
            $contact = $client->contacts()->create([
              'phone' => '+79991234567',
              'name' => 'Test',
            ]);

Внутри нашей openapi.yaml — 92 таких блока, по три-четыре языка на каждый осмысленный endpoint. Меняется сигнатура — правка в одном месте, через рендер Redoc она автоматически попадает на страницу документации.

Опубликовали v1.0.0 и через неделю наступили на грабли — из тех, что понятны только на практике:

  • Качество спеки определяет качество SDK. Поле event в webhook-payload было описано как oneOf без discriminator. Генератор не смог построить корректный union-тип, на выходе — any в Node SDK. Спека — это не отчёт о том, что есть, а контракт, по которому работает генератор: любая неточность всплывает во всех SDK одновременно. Прошли весь openapi.yaml заново, выправили discriminator, привели типы дат к единому формату.
  • Шаблоны генератора — точка для кастомизации. Стандартный PHP-шаблон генерирует код без strict types. Сначала патчили сгенерированный код руками — правки терялись при следующей регенерации. Правильный путь — переопределить шаблоны: у нас папка openapi-templates/ с Mustache-модификациями, и правка живёт там, а не в сгенерированном коде.
  • Нестандартные части пишутся поверх генерации. Полная генерация покрывает структуру: типы, сигнатуры, сериализацию. Особые вещи (Laravel-facade, реконнект long-polling) кладутся отдельно. Архитектурный приём: сгенерированный код — в src/Generated/, ручные надстройки — в src/Extension/. При регенерации src/Generated/ перезаписывается целиком, src/Extension/ остаётся нетронутым; конфликт становится явным при сборке.

Что бы сделали иначе: перешли бы на генерацию с самого начала, без эпизода ручных SDK, — сэкономили бы три месяца. Хотя без опыта ручного SDK, возможно, не поняли бы, насколько важна точность спеки.

White-label: когда API хочет стать платформой

После стабилизации SDK появилась следующая аудитория — интеграторы. Не прямые клиенты, а агентства и ISV, встраивающие наш API в свои продукты под собственным брендом. У прямого клиента контур простой: один аккаунт, один проект, несколько ключей, один webhook. У партнёра — многоуровневый: сам партнёр сверху, внутри десятки или сотни конечных клиентов, у каждого свой sub-проект со своими данными, биллингом, webhook'ом.

Сначала пытались обслуживать партнёров тем же API через флаги «если ты партнёр — можешь передать ещё этот параметр». Получилось мутно: семантика запутывается, тесты пишутся под два варианта одного эндпоинта, документация раздваивается. Переделали через отдельный префикс /partner/v1/* — обособленный набор эндпоинтов для партнёрских операций. Минимальный набор свойств, которые заложили:

  • Партнёрский уровень аутентификации — отдельный тип ключа (partner key) с более высокими привилегиями, чем у обычного project key. Через него можно создавать новые проекты, перечислять существующие и получать ключи для каждого. Обращение к этим эндпоинтам обычным project key возвращает 403.
  • Sub-проекты под одним партнёрским аккаунтом — иерархия: партнёр сверху, набор конечных проектов снизу, у каждого свои данные, webhook, биллинг.
  • OAuth installations с partner-scope — для партнёров, строящих маркетплейс на нашем API. Приложение, установленное в партнёрский аккаунт, через partner-scope видит все sub-проекты, а не только тот, в который установлено напрямую.
  • Биллинг-события через API — партнёр биллит клиентов по своей модели, ему нужны сырые usage-данные по каждому sub-проекту: эндпоинт GET /partner/usage плюс webhook-события в конце расчётного периода.
  • Audit-логи на партнёрском уровне — когда конечный клиент жалуется, партнёр диагностирует сам, без обращения к нам; доступ к журналу по sub-проектам регулируется тем же partner-ключом.

Изоляция данных — критическая граница, а не «фича». Внутри одного партнёрского аккаунта могут оказаться sub-проекты конкурирующих компаний, и возможность одного увидеть данные другого — катастрофа. Граница держится на трёх уровнях сразу:

php
// уровень БД — фильтр в каждом запросе
WHERE project_id = ?

// уровень API — явный project_id в запросе + проверка принадлежности
Auth::assertProjectAccess($projectId);

Auth::assertProjectAccess($projectId) вызывается в начале каждого эндпоинта, работающего с проектом. Партнёрский уровень добавляет ещё один слой: проверяется не только доступ к sub-проекту, но и то, что sub-проект принадлежит партнёрскому аккаунту, выпустившему ключ. Без этой защиты данные потекут между проектами на первой же ошибке в коде.

Документация для партнёра — отдельная: своя Postman-коллекция с разделом «Partner API» (создание sub-проекта, получение ключа, перечисление проектов, выгрузка usage), не подмножество обычной коллекции. Что осталось вне контура — кастомные домены для брендирования UI: пока в плане, текущая партнёрская работа идёт через API без кастомного фронтенда. Полный разбор reseller-модели, sub-проектов и OAuth-scope — в статье Partner API и reseller: sub-проекты и изоляция.

Open-source: спор, в котором мы чуть не приняли неверное решение

К моменту, когда SDK стали стабильными, документация написана, а white-label работал, встал вопрос об открытии исходников. Спор был серьёзный. С одной стороны — «зачем тратить ресурс на публичный репозиторий, у нас маленькая команда, контрибьюций не будет». С другой — «закрытый SDK на горизонте года превратится в якорь».

Был момент, когда мы почти решили не открывать: писать CONTRIBUTING.md, отвечать на issues, следить за качеством PR — реальная работа. И тут один из ребят задал вопрос, который перевернул обсуждение: какова первая реакция корпоративной security-команды, когда в продакшен ставится закрытая библиотека от внешнего вендора?

Заблокировать. Самый частый исход — разрешать чёрный ящик в проде против их природы. Альтернативы дорогие: SOC 2 Type II (десятки тысяч долларов и месяцы) или исходный код по NDA (недели на процедуру). Стало ясно: закрытый SDK ограничивает нас на корпоративном сегменте именно сейчас, когда мы туда идём. Это не «дать ли подарок сообществу», а «убрать ли якорь, который мешает расти». Развёрнуто все пять функций open-source SDK разобраны отдельно — Зачем открывать SDK в open-source: 5 причин.

Открыли. В организацию github.com/BOTIX-pro:

  • sdk-php (Composer botix-pro/sdk), sdk-python (PyPI botix), sdk-node (npm @botix/sdk), cli (PyPI botix-cli);
  • три example-репозитория под популярные фреймворки: example-laravel, example-express, example-flask.

Лицензия MIT, CONTRIBUTING.md в каждом репо, тесты в CI на GitHub Actions. Установка — три строки, один и тот же API в трёх экосистемах:

composer require botix-pro/sdk:^1.1
pip install botix==1.1.0
npm install @botix/sdk@1.1.0

Структура методов одинакова на всех языках: client.contacts.create(...) работает так же в Python, Node и PHP — переключаясь между языками, не переучиваешься. Это не совпадение, а сознательная работа, возможная только потому, что все три SDK собраны из одной спеки. Дисциплина, которая делает открытие безопасным: SDK не содержит бизнес-логики. Он делает ровно три вещи — формирует запрос, подписывает его, парсит ответ. Всё остальное (индекс категорий, бизнес-правила, расчёты) лежит на стороне API, поэтому открытый код не раскрывает ничего, кроме формата работы с публичным API, который и так опубликован в OpenAPI-спецификации.

Что знаем точно: контрибьюций пока нет, SDK свежие, оценку популярности снимем через 90 дней по установкам и звёздам. Что открытие дало сразу: когда приходим к корпоративному клиенту и говорим «вот наш SDK, посмотрите код перед интеграцией», встреча с security становится спокойнее — не «архив по NDA через неделю», а «открыли GitHub, посмотрели, пропустили».

Что бы мы сделали иначе

Если бы начинали путь от внутреннего API до открытой платформы сегодня:

  • OpenAPI как точка правды с первого дня — не описание задним числом.
  • Шаблоны генератора с самого начала — не патчить сгенерированный код руками.
  • Партнёрский префикс отдельным набором эндпоинтов сразу — не флаги поверх обычных.
  • Open-source с релиза 1.0, не 1.1 — закрытие SDK на ранней стадии = фальстарт для корпоративной аудитории.
  • CONTRIBUTING.md в день публикации, не через месяц.

Все пять — вещи, которые мы сделали неправильно в первой итерации и переделали во второй.

Что в итоге

Год пути от внутреннего API до открытой платформы. Три SDK без зависимостей, четыре публичных репозитория, openapi.yaml как точка правды, cursor-пагинация и rate-limit headers, OAuth, white-label через /partner/v1/*, отдельная партнёрская Postman-коллекция, audit-логи в кабинете. Про то, как выпускать обновления SDK по semver, не ломая партнёрские интеграции, — в разборе Версии SDK и semver: обновления без breaking.