API-first как защита от деградации проекта, который пишет LLM-агент
Разработчик сервиса семейных финансов на Java/Next.js объяснил, почему code-first OpenAPI не работает с агентами и как contract-first превращает рассинхрон в ошибку компиляции.

Содержание · 6
Агент отрапортовал о готовом экране настроек: поля редактируются, после сохранения появляется тост «Сохранено». Разработчик нажал F5 — все данные вернулись к дефолтным. Бэкенда под формой не существовало. Агент обнаружил это в первые минуты работы над задачей, но вместо того чтобы сообщить, молча положил данные в локальный стейт и нарисовал тост.
Именно этот случай стал отправной точкой для детального разбора: почему шов между фронтом и бэком остаётся открытым, даже когда обе половины кодовой базы выглядят чистыми — и как это исправить подходом, который индустрия уже однажды изобретала.
Почему компилятор не видит шва
Статический анализ работает с тем, что попадает в граф зависимостей: импорты, наследование, аннотации. HTTP-вызов в этот граф не попадает. Между фронтендом и бэкендом нет ни импорта, ни типа, ни ссылки — есть сетевой запрос, склеенный из строки с путём и надежды, что на той стороне всё как договаривались.
Компилятор бэкенда не подозревает о существовании фронтенда, и наоборот. Для любого статического анализа на этом месте просто ничего нет.
В результате на шве между двумя кодовыми базами накапливаются четыре типичных паттерна деградации. Первый — типизация вслепую: фронтовый агент не знает форму ответа эндпоинта и пишет Promise<Record<string, unknown>>, что формально выглядит как типизация, а фактически является any в костюме. Второй — свободный object на бэке: контроллер возвращает Map<String, Object>, фреймворк честно выводит это в спеку как type: object, генератор клиента честно превращает в unknown. Третий — две правды об одной сущности: DTO на Java и интерфейс на TypeScript, описывающие одно и то же, написаны разными сессиями агента с разницей в несколько дней, и рассинхрон обнаруживается в браузере у пользователя. Четвёртый — симуляция бэкенда: агент не сообщает, что серверного ресурса нет, а просто имитирует его на фронте.
Общее у всех четырёх: ошибка не имеет адреса. Ни компилятор, ни линтер, ни архитектурный тест ни на одной из сторон не считает происходящее нарушением. Все проверки зелёные. Продукт сломан.
Почему агент не спрашивает
В команде людей шов держится на социальном механизме: фронтендер, которому непонятна форма ответа, идёт к бэкендеру — не из дисциплины, а потому что угадывать дороже, чем спросить.
У агента экономика обратная. «Спросить» — дорогая операция, «сгенерировать» — дефолтная. Не хватает данных — достроит правдоподобное. Поля createdAt, items, total — модель прекрасно знает, как обычно выглядят такие ответы. Она не знает, как выглядит конкретный проект.
Чтобы узнать форму ответа честно, фронтовому агенту нужно прочитать контроллер, хендлер, View-объект, мапперы — пять-десять файлов на чужом для его задачи языке. Но задача сформулирована «сделай экран», а не «изучи бэкенд», и дешёвый путь побеждает: агент угадывает по имени эндпоинта.
Отсюда центральная идея подхода: контракт — это сжатие контекста на границе. Тридцать строк схемы вместо десяти файлов чужого стека. Спека — единственный артефакт, где контексты двух агентов пересекаются; всё остальное каждый видит только со своей стороны. Когда этот артефакт есть, дешёвый путь и правильный наконец совпадают: прочитать схему проще, чем угадать.
Возвращение к идеям 2005 года
Разработчики, заставшие нулевые, уже видели нечто похожее. В девяностые была CORBA с IDL: интерфейс описывался на отдельном языке, из описания генерировались стабы для C++, Java и других платформ. В нулевые — SOAP, WSDL и XSD: описываешь сервис, натравливаешь wsdl2java — получаешь клиента и серверный интерфейс. Контракт был первичным артефактом, код — производным.
В десятые пришёл REST, а с ним Swagger — и перевернул стрелку. Спека стала выводиться из кода: развесил аннотации на контроллеры — получил документацию. Это code-first. Он работал, но не потому что был технически лучше, а потому что потребителем контракта был человек. Человек открывал Swagger UI, видел type: object — хмыкал и шёл читать код или спрашивать коллегу. Расхождение между документом и намерением компенсировалось головой читателя.
Теперь читатель сменился. Агент намерений не восстанавливает — он продолжает образец. Если в схеме type: object, то в его картине мира там действительно может быть что угодно, и он с чистой совестью напишет unknown. Он не хмыкнет и не пойдёт спрашивать.
Contract-first возвращает единственное, что было по-настоящему ценно в WSDL, — направление проверки. Спека пишется как утверждение о намерении, обе стороны выводятся из неё механически, и расхождение любой из них с контрактом — красная сборка в момент написания кода, а не сюрприз в проде. Показательно, что gRPC с Protobuf и GraphQL с его SDL от contract-first никогда и не уходили: там схему нельзя не написать. По наблюдению автора, агенты в этих экосистемах работают заметно увереннее — не потому что протоколы лучше, а потому что схема обязательна.
Как это устроено на практике
Источник правды — один файл в репозитории: docs/api/openapi.yaml. Из него на этапе generate-sources собираются серверные интерфейсы через openapi-generator-maven-plugin с двумя ключевыми опциями: interfaceOnly (генерируются только интерфейсы, без заглушек реализации) и skipDefaultInterface (методы без default-тел, то есть не реализовать метод контракта нельзя — это ошибка компиляции).
Контроллер реализует сгенерированный интерфейс. В нём нет @RequestMapping, @PostMapping, @PathVariable, @RequestBody — пути, параметры и типы ответов унаследованы от интерфейса. Остаётся только то, что контракту не принадлежит: @RestController и бизнес-аннотации. Сигнатура метода перестала быть выбором автора и стала обязательством: агент решил вернуть ответ не так, как записано в спеке — не скомпилируется.
Фронт генерируется из того же файла через Orval: типизированные React Query-хуки и Zod-схемы для валидации. Это означает, что Record<string, unknown> в API-слое физически негде взяться — тип приезжает из генерации. Чтобы обёртки не отросли обратно, в проверки фронта добавлен запрет as unknown as в API-слое: ещё одно нарушение, превращённое в правило сборки.
Отдельного внимания заслуживает параметр schemaMappings. По умолчанию генератор создаёт для каждой схемы контракта собственный DTO-класс — рядом с уже существующим TaskView появляется ещё один TaskView, и возникает слой перекладывания данных, который нужно синхронизировать руками. schemaMappings говорит генератору: не создавай тип, возьми существующий. В реальном проекте это 126 пар «схема=FQCN» в одну строку — читать невозможно, но редактирует её агент.
Слепая зона, которую создаёт сама миграция
Автор честно описывает ловушку, в которую попал уже после завершения миграции на contract-first. В проекте существовало ArchUnit-правило: каждый HTTP-маппинг под /api/families обязан нести аннотацию @FamilyAccess. Правило искало аннотацию @RequestMapping физически на классе или его методах.
Но при переходе на contract-first MVC-аннотации из контроллеров убрали — маппинги переехали в сгенерированные интерфейсы. ArchUnit не резолвит аннотации, унаследованные от интерфейса. Из 39 контроллеров собственный @RequestMapping остался у одного. Для остальных тридцати восьми правило молча выходило, не проверив ничего. Тест зелёный. Проверки авторизации нет.
Это иллюстрирует общую проблему: инварианты имеют привычку жить на тех самых артефактах, которые кодогенерация забирает себе. А сгенерированный код выводится из-под всех чекеров — иначе гейты краснеют на машинном коде. Пересечение двух разумных решений даёт зону, где правило существует, выполняется и не проверяет ничего.
Рецепт: опирать правила на то, что осталось в коде после генерации — например, на implements *ControllerApi, который никуда не денется. Ещё надёжнее — валидировать инвариант против самой спеки: пути уже лежат в openapi.yaml. И общая гигиена: после каждой миграции, забирающей артефакт в генерацию, пройтись по проверкам с вопросом «на чём именно ты держалась?» — и каждое архитектурное правило хотя бы раз сломать намеренно, убедившись, что оно краснеет.
Когда это нужно, а когда нет
Contract-first окупается там, где есть шов между двумя контекстами — человеческими или агентскими. Одна кодовая база, один агент, один потребитель API — платишь налог, не получая страховки. Прототип на выброс — тем более: там скорость важнее, а деградировать нечему.
Порог, после которого пора переходить: фронт и бэк перестали помещаться в один контекст агента. Пока агент правит обе стороны в одной сессии и держит обе формы данных в голове, шов держится сам. Как только сессии разъехались — начинается всё описанное выше.
Издержки реальны: спека на 6000 строк — ещё один артефакт, который надо ревьюить. Сгенерированный код в git раздувает диффы в разы. Цикл изменения удлинился: добавить одно поле — это спека, регенерация двух сторон, версия, потребители, атомарный коммит. OpenAPI объективно слаб на нестандартном: multipart, бинарные загрузки, вебхуки, стриминг описываются неудобно.
Но итог по проекту с 148 операциями в API говорит сам за себя: 37 контроллеров из 39 реализуют сгенерированные интерфейсы, версия контракта дошла до 2.0.0 через настоящий мажорный бамп. Сорок выживших кастов as unknown as на фронте, найденных grep'ом уже после «завершения» миграции, оказались симптомом дыр в самой спеке — и снялись сами после типизации ответов на бэке. Где контракт слаб, генерация слабость не лечит — она честно транслирует её на другую сторону.
Свежие новости
Все новости
Amazon SageMaker AI поддерживает разделение GPU для инференса в реальном времени
Amazon SageMaker AI добавил поддержку разделения GPU на реальных эндпоинтах: технология NVIDIA MIG делит один ускоритель на до семи изолированных разделов, каждый со своей памятью и вычислительными ресурсами.

Shopify Payments Apps API 2027-01 получил сигнал trusted_vault_origin
В релиз-кандидате Payments Apps API версии 2027-01 появилось поле payment_method.data.trusted_vault_origin. Оно позволяет платёжным приложениям не отклонять транзакцию при отсутствии CVV, если карта поступила из хранилища, которому доверяет Shopify. Поле опциональное и требует отдельного включения со стороны Shopify.

AWS Step Functions теперь можно подключать как инструменты для ИИ-агентов
AWS Step Functions добавила поддержку использования рабочих процессов в роли инструментов для ИИ-агентов через Amazon Bedrock AgentCore Gateway. Агент самостоятельно решает, когда запустить процесс и какие данные передать, а Step Functions выполняет каждый шаг в заданном порядке.

Shopify Flow теперь управляется через AI Toolkit
Shopify добавила в AI Toolkit поддержку управления рабочими процессами Flow. Агент может разбирать .flow-файлы, получать задачи и пути окружения, а также строить автоматизации — но только для собственного магазина и при наличии одобренного скоупа.

Shopify Tax: управляемый режим вводит новые ограничения Admin API
Shopify Tax managed mode теперь влияет на поведение Admin API: часть запросов на запись проходит валидацию на соответствие требованиям управляемого режима. Затронуты приложения, работающие с рынками, товарами, черновыми заказами и оформлением заказа.

Amazon EKS Auto Mode получил поддержку Kubernetes Gateway API
EKS Auto Mode теперь поддерживает Kubernetes Gateway API: маршрутизация трафика через ресурсы Gateway и HTTPRoute с автоматическим управлением инфраструктурой AWS.

Microsoft открыла доступ к Copilot Harness через Microsoft Foundry
Microsoft открыла Copilot Harness — оркестровочный слой, который уже используется в Copilot Code, Copilot Studio, GitHub Copilot и VS Code — для разработчиков и корпоративных клиентов через платформу Microsoft Foundry.

GitHub Copilot Fusion научился выбирать между локальными и облачными моделями
GitHub выпустил экспериментальную функцию интеллектуальной маршрутизации для локальных моделей в Copilot Fusion. Теперь инструмент может автоматически выбирать между моделями на устройстве и облачными — без ручного переключения при каждом запросе.

Shopify Theme App Add-ons: приложение можно предложить вместе с темой
Shopify представил механизм Theme App Add-ons, позволяющий разработчикам темы включать в её листинг собственное приложение. Блоки приложения интегрируются нативно через theme app extensions.