Генерация 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, каждую модель, каждый код ошибки, каждое поле запроса и ответа. Из него генерируется всё остальное.
Цикл выпуска новой версии теперь выглядит так:
- Правка в
openapi.yaml(новый endpoint, новое поле, новый код ошибки). openapi-generator-cliс пресетом для PHP → обновлённыйsdk-php.- То же для Python →
sdk-python. - То же для Node →
sdk-node. - Из той же
openapi.yamlрендерится Redoc — публичная документация наdevelopers.botix.pro. - Из той же
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:
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-проекты конкурирующих компаний, и возможность одного увидеть данные другого — катастрофа. Граница держится на трёх уровнях сразу:
// уровень БД — фильтр в каждом запросе
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(Composerbotix-pro/sdk),sdk-python(PyPIbotix),sdk-node(npm@botix/sdk),cli(PyPIbotix-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.
Попробуйте API BOTIX
Получите ключ, отправьте первый запрос и поймайте webhook — прямо в браузере, без боевого канала.
Попробовать API бесплатноЧитайте также
Zero-dependency SDK, open-source и партнёрский API
API как платформа: multi-tenant, white-label и open SDK
Как публичный API становится платформой для интеграторов
Версии SDK и semver: как выпускать обновления, не ломая партнёрские интеграции
