Попробовать API
</>
Почему в SDK ноль зависимостей и генерация из OpenAPI
sdkВозражение
15 мин

Почему в 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 и расширений:

json
{
  "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 — целиком, как он лежит в проде:

python
# 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 None

urllib.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") в корне репозитория платформы. Цикл выпуска версии:

  1. Изменения в openapi.yaml — новый endpoint, поле или код ошибки.
  2. openapi-generator-cli с пресетом PHP → обновлённый sdk-php.
  3. То же для Python → sdk-python.
  4. То же для Node → sdk-node.
  5. Из той же спеки рендерится Redoc — публичная документация на developers.botix.pro.
  6. Из той же спеки — 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:

yaml
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 не примет фикс:

json
{
  "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).