Почему в SDK ноль зависимостей и генерация из OpenAPI
TL;DR: когда интегратор ставит чужой SDK, он ставит не код, а граф зависимостей. Один SDK с `guzzlehttp`, `psr/log` и `symfony/options-resolver` ломает версии в монолите заказчика, а через год обновляется раз в полгода. У нас в `botix-pro/sdk` — zero-deps. Разбираю три связанные практики, на которых это держится: HTTP через stdlib, генерация из `openapi.yaml`, open-source как технический канал доверия.
Коротко
- Zero-dependency. SDK — не приложение, а тонкая обёртка над HTTP. Каждая его зависимость — конфликт версий, вектор supply-chain и мина устаревания у интегратора в проде. Решение — только stdlib:
curlв PHP,urllibв Python,httpsв Node. Секцииdependenciesпустые. - Генерация из OpenAPI. Ручной SDK на трёх языках отстаёт тремя способами — documentation / type / coverage drift. Один
openapi.yaml+openapi-generator-cli= шесть артефактов (три SDK, Redoc, Swagger, x-codeSamples) из одного источника правды. - Open-source. Открытый SDK — не «щедрость», а инструмент: снимает блокировку security-команды заказчика, разблокирует bug fix через форк/PR, работает сигналом зрелого API. Всё выложено в
github.com/BOTIX-pro.
Все три практики привязаны к одной идее: SDK живёт не в нашем репозитории, а в продакшене у интегратора. Его жизненный цикл начинается тогда, когда мы про него думать перестаём. Поэтому каждое проектное решение оцениваем не «как нам удобнее писать», а «во что это обойдётся тому, кто это установит».
Тезис 1. Zero-dependency: что подключается вместе с SDK
Команда подключает к своему сервису сторонний API. Заходит на страницу документации, видит composer require vendor/sdk (или pip install vendor-sdk, npm install @vendor/sdk), устанавливает. Через минуту в composer.lock появляется не одна новая строка, а четырнадцать. SDK притащил HTTP-клиент, JSON-валидатор, библиотеку для UUID, retry-механизм, ещё один логгер и пару утилитарных пакетов, которые поддерживает один мейнтейнер в свободное время. Каждая из этих зависимостей теперь часть приложения интегратора, и каждая создаёт ровно три класса проблем.
Конфликты версий. SDK тянет guzzlehttp/guzzle: ^6.5. У интегратора уже ^7.0 из-за собственного фреймворка. Пакетный менеджер пытается согласовать версии: жёсткий pin — установка падает с конфликтом; мягкий (>=2.20,<3.0) — установится одна из совместимых, и через полгода обновление фреймворка ломает совместимость с SDK. В Python особенно болезненно: глобальное пространство имён пакетов не позволяет держать две версии requests одновременно. В Node за счёт node_modules per-package чуть легче, но дубли раздувают бандл.
Supply-chain атаки. Каждая транзитивная зависимость — вектор атаки. event-stream в npm — мейнтейнер передал доступ незнакомцу, тот добавил код для кражи криптокошельков. ua-parser-js — взлом аккаунта, релиз с майнером. colors.js — «протест-релиз», ломающий всё, что от него зависит. Когда SDK тянет 14 зависимостей, дерево транзитивных пакетов переваливает за 100 — и npm audit / safety check / composer audit начинают регулярно ругаться на advisory в зависимостях зависимостей, которые интегратор пофиксить не может: он не контролирует SDK, через который они пришли.
Устаревание. Зависимость, что год назад была свежей, сегодня имеет известную уязвимость. Активно поддерживаемый SDK обновляет их — нормальный жизненный цикл. Поддерживаемый лениво или заброшенный — превращает каждую зависимость в мину под интегратором.
Альтернатива — zero-dependency: SDK использует только стандартную библиотеку языка. В PHP — curl через расширение из стандартной поставки, в Python — urllib.request из stdlib, в Node — встроенные https и http. composer.json пакета botix-pro/sdk v1.1.0 не имеет ни одной зависимости в require кроме php и расширений:
{
"name": "botix-pro/sdk",
"version": "1.1.0",
"require": {
"php": ">=8.1",
"ext-curl": "*",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"phpstan/phpstan": "^1.10"
},
"autoload": {
"psr-4": { "BOTIX\\SDK\\": "src/" }
}
}requirements.txt Python-пакета botix v1.1.0 пустой. package.json Node-пакета @botix/sdk имеет нулевую секцию dependencies — только devDependencies для билд-инструментов, которые в продакшен не попадают.
Возражение «requests удобнее urllib». Справедливо — для прикладного кода. SDK прикладным кодом не является. Он живёт у интегратора в продакшене, и его задача — не быть красивым, а минимизировать риск для того, кто его установит. Удобство «нет 14 проблем с зависимостями» переоценивает удобство «синтаксис чуть приятнее».
Что реально становится сложнее. HTTP-клиент в stdlib покрывает 95% сценариев публичного API: GET/POST/PUT/DELETE, заголовки, тело, чтение ответа. HTTP/2 multiplexing или connection pooling для SDK не нужны — публичный API через интернет работает на HTTP/1.1 без потерь. Сложнее становятся ровно три вещи: моки в тестах (пишутся вручную — решается шаблонами генерации), документация чуть длиннее (urllib.request.Request(url) вместо requests.get(url) — всё ещё несколько строк, не сотни), retry-логика (цикл с экспоненциальным backoff пишется за полчаса). Взамен: установка перестаёт быть событием в CI/CD, npm audit молчит, бандл Node-приложения растёт на 30–40 КБ, а не на 2 МБ.
Вот HTTP-вызов в Python-SDK через нативный urllib — целиком, как он лежит в проде:
# github.com/BOTIX-pro/sdk-python/blob/v1.1.0/src/botix/_http.py
import json
import urllib.request
import urllib.error
from urllib.parse import urlencode
class _HttpClient:
def __init__(self, base_url: str, api_key: str, timeout: int = 30):
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.timeout = timeout
def request(
self,
method: str,
path: str,
body: dict | None = None,
params: dict | None = None,
headers: dict | None = None,
) -> dict:
url = f"{self.base_url}{path}"
if params:
url = f"{url}?{urlencode(params)}"
data = json.dumps(body).encode("utf-8") if body is not None else None
req_headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"User-Agent": "botix-python-sdk/1.1.0",
**(headers or {}),
}
req = urllib.request.Request(url, data=data, method=method, headers=req_headers)
try:
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
payload = resp.read().decode("utf-8")
return json.loads(payload) if payload else {}
except urllib.error.HTTPError as e:
# Парсинг тела ошибки — там лежит код и request_id из ErrorResponse
err_body = e.read().decode("utf-8")
raise self._build_error(e.code, err_body) from None
except urllib.error.URLError as e:
raise ConnectionError(f"network error: {e.reason}") from Noneurllib.request, json, urllib.parse — всё из stdlib Python 3.8+. Ключ в заголовке — Authorization: Bearer XXXXXXXX… (публиковать реальную строку нельзя, в примерах всегда плейсхолдер). На production за два года эксплуатации этот код не правился. Тренд не наш — Stripe, Cloudflare, GitHub-команды двигают официальные SDK в ту же сторону: каждая лишняя зависимость в пакете — это лишняя кнопка тревоги в проде у тысяч интеграторов. Про то, где в такой обёртке хранить и как ротировать сам ключ — отдельный разбор zero-dependency SDK и хранение API-ключей.
Тезис 2. Генерация из OpenAPI, а не руками
Zero-deps упрощает генерацию: когда есть единственный источник правды — openapi.yaml — и шаблон генератора, привязанный только к stdlib, регенерация выглядит как одна команда. Ручной SDK на нескольких языках ломается тремя неизбежными способами.
Documentation drift. Endpoint добавили в код сервера и в доку. В PHP-SDK метод появится через две недели, в Python — через месяц, в Node — через два. В окне между этим клиенты на разных языках видят разный набор возможностей, и приходит тикет «в доках метод X есть, в SDK нет — это баг».
Type drift. Сервер отдаёт created_at строкой ISO 8601. В PHP типизировали как string, в Python — datetime, в Node — Date | string. Меняем формат на Unix timestamp: на сервере — один коммит, в трёх SDK — три PR, три релиза, три обновления доки, и вероятность, что они пройдут синхронно, стремится к нулю.
Coverage drift. Сервер — 60 endpoints, PHP-SDK — 60, Python — 55, Node — 47. Появляется метод №61 — у него три разных приоритета в трёх командах. Держать паритет ручным трудом реально только с отдельным человеком, единственная задача которого — следить за паритетом.
Single source of truth закрывает все три. У нас единственный источник правды о Public API — файл openapi.yaml (OpenAPI 3.0.3, info.version: "1.1.0") в корне репозитория платформы. Цикл выпуска версии:
- Изменения в
openapi.yaml— новый endpoint, поле или код ошибки. openapi-generator-cliс пресетом PHP → обновлённыйsdk-php.- То же для Python →
sdk-python. - То же для Node →
sdk-node. - Из той же спеки рендерится Redoc — публичная документация на
developers.botix.pro. - Из той же спеки — Swagger UI на
developers.botix.pro/try-it, интерактивная песочница.
Один файл — шесть артефактов. Генерация запускается через Docker, шаблон отработан ещё в v1.0.0:
docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate \
-i /local/openapi.yaml \
-g <php|python|typescript> \
-o /local/sdk-<lang> \
--additional-properties=<options>Схематично это выглядит так — один вход, три пакета в трёх реестрах:
openapi.yaml (v1.1.0)
|
↓
openapi-generator-cli
templates/php
templates/python
templates/node
|
+---------------+---------------+
| | |
↓ ↓ ↓
sdk-php sdk-python sdk-node
(Composer) (PyPI) (npm)
| | |
↓ ↓ ↓
botix-pro/sdk botix @botix/sdk
v1.1.0 1.1.0 1.1.0Отдельная история — код-сэмплы в документации. Обычно их пишут вручную в 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 XXXXXXXX…" \
-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 автоматически попадает на страницу документации.
Что держать в голове при генерации. *Качество спеки определяет качество SDK*: oneOf без discriminator → генератор не построит union-тип в TypeScript; код ошибки только в description, а не в responses → клиент не научится его обрабатывать. Спека — не отчёт «что есть», а контракт, по которому работает генератор: любая неточность всплывёт во всех SDK разом. *Шаблоны генератора — точка кастомизации*: нет strict types или timing-safe сравнения в verifyWebhook — правится один раз в шаблоне, в сгенерированный код руками не лезем. *Версия SDK ≠ версия API*: API 1.1.0, SDK может быть 1.1.2 (патчи шаблонов), связь держим через MIGRATION.md. *Поведенческие тесты пишутся отдельно* от генератора, и их объём в разы меньше ручного SDK.
Когда генерация не подходит. Нестандартные паттерны — long-polling с особой логикой реконнекта, batch со склейкой запросов на клиенте, фреймворк-адаптеры (Laravel facade, Django app) — пишутся руками поверх сгенерированной базы. Приём: сгенерированное — в src/Generated/, ручные надстройки — в src/Extension/. При регенерации src/Generated/ перезаписывается целиком, src/Extension/ не трогается; конфликт становится явным на сборке. Смежная тема надёжности контракта — идемпотентность и retry на клиенте — в Idempotency-Key и повторные POST.
Тезис 3. Open-source как канал доверия
Третья практика — SDK на GitHub под открытой лицензией. В большинстве команд это обсуждают через категорию «щедрость»: «принесём пользу сообществу» против «лучше потратим время на фичи». Обе стороны рассуждают о морали, а не о деле. На практике open-source SDK — инструмент с конкретными функциями.
Функция 1: security-аудит без участия поставщика. Корпоративная интеграция проходит через security-команду. Закрытая библиотека — чёрный ящик, и дальше три дорогих варианта: заблокировать (задача не решена), потребовать SOC 2 Type II + ISO 27001 + DPA (десятки тысяч долларов и месяцы поставщику), запросить код по NDA (недели обеим сторонам). Open-source закрывает все три: security открывает репозиторий, видит, что библиотека делает только заявленное, и пропускает интеграцию. SOC 2 всё равно нужен — но SDK как точка входа в чужой код перестаёт быть чёрным ящиком.
Функция 2: bug fix не блокируется вендором. В закрытом SDK ошибка — тикет в поддержку и релиз через месяц-три; всё это время у интегратора костыль или нерабочая интеграция. В open-source два пути: сделать PR либо временно форкнуть и патчить через VCS-ссылку, пока upstream не примет фикс:
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/myorg/sdk-php-fork" }
],
"require": {
"botix-pro/sdk": "dev-fix-timeout as 1.1.0"
}
}Функция 3: сигнал зрелости API. Открытый SDK — сигнал стабильного контракта, который не стыдно показать. Закрытый читается как «наколхозили, поэтому прячут» — не обязательно правда, но типовое прочтение. Сигнал работает не на сознательном уровне: покупатель говорит «знакомая компания, серьёзные люди» после того, как его инженер посмотрел репозиторий и не нашёл ничего стыдного. У нас это встроено в стратегию SDK с самого начала — все публичные ресурсы в одной организации:
github.com/BOTIX-pro
├── sdk-php # Composer botix-pro/sdk v1.1.0
├── sdk-python # PyPI botix 1.1.0
├── sdk-node # npm @botix/sdk 1.1.0
├── cli # PyPI botix-cli 1.0.0
├── example-laravel # Laravel 11 + Pest пример
├── example-express # Express 4 + TypeScript + Vitest
└── example-flask # Flask 3 + pytestУстановка — три строки, по одной на реестр:
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 бесполезен без API, к которому обращается; под чужой API его легче переписать, чем переиспользовать. Страх «раскроем архитектуру» снимается дисциплиной — SDK не содержит бизнес-логики, делает ровно три вещи: формирует запрос, подписывает его, парсит ответ; всё остальное (индекс категорий, бизнес-правила, расчёты) — на стороне API и так публикуется в OpenAPI-спеке. Реальная стоимость одна — ресурс на ведение репозитория: без выделенного человека open-source SDK быстро становится кладбищем с просрочкой по 200 дней. Про webhook-подпись и верификацию событий на стороне интегратора — проверка HMAC-подписи webhook.
Честно: что пока не получилось
Контрибьюций от сообщества нет. SDK молодые (опубликованы 2026-05-22), внешних PR пока ноль — это нормальный начальный режим односторонней отдачи. Оценка через 90 дней. Человек на review issues назначен, регламент ответа — 3 рабочих дня. Документация по контрибьюциям тонкая — в CONTRIBUTING.md пока базовое. Bypass 2FA для npm publish — аккаунт с passkey-2FA требует галочку «Bypass two-factor authentication» в granular-токене; без неё npm publish падает с 403 Forbidden (узнали на болезненном опыте). На PyPI такого нет.
Чек-лист перед установкой чужого SDK
Перед composer require / pip install / npm install откройте package.json / composer.json / setup.py SDK на GitHub и посмотрите размер секции dependencies:
- 0–2 пакета — SDK с уважением к интегратору.
- 5–7 — приемлемо, но проверьте, что это мейнстрим, а не маргинальные пакеты от одного автора.
- 10+ — SDK, который кладёт на интегратора груз поддержки чужой экосистемы.
Каждая практика по отдельности — экономия часа в год. Все три вместе — другая модель работы SDK, при которой команда из двух человек поддерживает три языка без отставаний. Смежные грани — версионирование контракта и его breaking-change (эволюция API без ломки клиентов) и обработка ошибок в клиенте (error-модель и retry).
Попробуйте API BOTIX
Получите ключ, отправьте первый запрос и поймайте webhook — прямо в браузере, без боевого канала.
Попробовать API бесплатноЧитайте также
SDK BOTIX 1.1: zero-deps, генерация из OpenAPI и триал
Первый запрос к API за 5 минут: SDK из OpenAPI и триал
Публичный API BOTIX: OpenAPI, multi-tenant и Partner API
Quickstart, триал-sandbox и polling vs webhook
Онбординг разработчика: quickstart, sandbox и API-ключи
