410 вариантов писем, сотня микросервисов и до 15 файлов, которые приходилось править ради одной нотификации. Добавление письма превратилось в квест: создай компонент, подключи, опиши параметры, зарегистрируй — и молись, что ничего не забыл.
Мы устали и автоматизировали это. Теперь описываем контракт письма в OpenAPI, а CLI генерирует весь boilerplate — от NestJS-артефактов до React-компонента с типизированными пропсами. Часы работы сжались до минут.
Меня зовут Денис, я фронтенд-разработчик в ЮMoney. В этой статье расскажу, почему мы пришли к кодогенерации, как устроили процесс и что в нём изменилось для разработчиков, дизайнеров и тестировщиков. Материал будет полезен тем, кто работает с React, NestJS или TypeScript и хочет перестать писать однотипный код вручную. Особенно если поддержка UI-компонентов и API-слоя отнимает время, которое лучше потратить на реальную логику.
Как письмо попадает к пользователю
У нас около сотни backend-микросервисов, и каждый в определённом бизнес-сценарии может отправить пользователю SMS, push или email. Для email схема выглядит так:

Микросервис передаёт оркестратору необходимые параметры. Оркестратор понимает, какое письмо нужно отправить, и запрашивает HTML у frontend-приложения. Оно подставляет данные в React-шаблон и возвращает готовую разметку, после чего оркестратор отправляет письмо. Само приложение серверное, на NestJS. Оно умеет генерировать около 410 вариантов писем. Новые письма мы пишем на React, старые постепенно мигрируют с BEM.

15 файлов ради одного письма
Исторически добавление новой нотификации выглядело так: найти нужные файлы, создать компонент, подключить его в модуле, описать параметры, добавить регистрацию и не забыть ещё несколько мест. В среднем для одного письма приходилось менять или создавать около 15 файлов.
Однажды при добавлении нескольких писем в одном PR мы забыли подключить их в модулях — и получили увлекательный квест по поиску причины, почему всё не работает. Но boilerplate был не единственной проблемой.
Когда одинаковые письма перестают быть одинаковыми
Большинство наших писем собрано из повторяющихся элементов: заголовок, описание, список, кнопки, ссылки, изображения. Казалось бы, всё просто — бери готовое решение и переиспользуй. Но на практике дизайнер мог как взять существующий компонент, так и собрать похожий блок с нуля. Разные команды выбирали разные пути, и одинаковые по задумке интерфейсы постепенно начали выглядеть по-разному.

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

Результат предсказуем: увеличились time-to-market и количество коммуникаций между разработчиками, дизайнерами и тестировщиками.
Сначала мы ограничили свободу
Решение началось с шаблонов.

Мы собрали существующие React-письма и вместе с дизайнерами выяснили: примерно 90% кейсов можно закрыть четырьмя шаблонами. Каждый шаблон уведомления состоит из изображения, заголовка, описания, кнопок и ссылок — необязательные элементы можно включать или выключать.

Главное здесь — не просто переиспользование кода. Шаблон инкапсулирует правила интерфейса: размеры, отступы, типографику и допустимые компоненты. При этом для оставшихся 10% писем возможность написать уникальный код никуда не делась.
Одна спецификация вместо поиска по микросервисам
Следующая проблема — параметры. Раньше, чтобы понять, какие данные приходят в конкретное письмо, иногда приходилось идти к бэкенд-разработчику. А если письмо принадлежало соседней команде, путь становился ещё длиннее.
Была у нас и историческая особенность: на входе NestJS все параметры приводились к нижнему регистру. Приложению 11 лет — когда-то это было необходимо, а теперь удалить такую логику безболезненно нельзя. В результате разработчик мог ожидать walletNumber, а получить walletnumber. Не лучший способ повысить читаемость кода.
Мы решили сделать единой точкой правды OpenAPI-спецификацию. Для каждого письма теперь есть схема с бизнес-сценарием, letterId, параметрами, их типами и обязательностью, примерами и внутренними параметрами для обратной совместимости.
Если уже всё описали — зачем писать руками?
OpenAPI содержит практически всё, что нужно для создания письма, а значит, boilerplate можно не писать вручную. Мы сделали CLI, которая читает спецификации и генерирует необходимые артефакты. Процесс выглядит так: OpenAPI → типизированная конфигурация → NestJS-сервис → React-компонент. CLI находит схемы, парсит YAML, приводит данные к типизированным объектам и на их основе создаёт сервисы. Заодно генерируются типы для React-пропсов.
Если параметр изменился в OpenAPI, а React-компонент забыли обновить — типы перегенерируются во время билда, и приложение упадёт уже на этапе сборки. Для новых писем мы также отключили историческое приведение параметров к нижнему регистру. Зачем менять имена, если спецификация уже задаёт контракт?
Как теперь добавить письмо
Представим, что нам нужно отправить участнику конференции персональное приветствие. У нас есть letterId, имя участника и текст письма. Запускаем CLI и создаём письмо: выбираем letterId и scope, после чего заполняем OpenAPI-схему. letterId генератор подставляет сам, нам остаётся описать имя участника — его тип, описание, пример и обязательность.
Затем запускаем генерацию и получаем NestJS-артефакты и React-компонент с типизированными пропсами. Остаётся заполнить динамический контент, открыть наш аналог Storybook — UI-doc — и проверить результат. Вся процедура занимает несколько минут вместо прежних нескольких часов.
Посмотреть на кодогенерацию на основе OpenAPI можно в записи моего доклада с митапа Frontend Mix.
А где AI?
Логичный вопрос: если мы автоматизируем рутину, почему бы просто не попросить AI написать письмо? Проблема в том, что модель может придумать не тот код или забыть важную деталь, а ревью такого кода превращается в головную боль.
Поэтому мы использовали AI иначе — попросили его помочь написать сам генератор. Дальше CLI работает одинаково для всех. Получился неплохой компромисс: AI помог создать инструмент, а инструмент обеспечивает детерминированный результат.
Что мы получили
Архитектура. OpenAPI стал контрактом и источником данных для генерации, код создаётся автоматически.
Консистентность. Общие элементы живут внутри шаблонов, поэтому одинаковые интерфейсы выглядят одинаково.
Скорость. Вместо ручного создания множества файлов разработчик описывает спецификацию и пишет только уникальный контент.
Коммуникация. Дизайнерам не нужно каждый раз рисовать знакомые паттерны — достаточно выбрать шаблон. Тестировщики и аналитики могут самостоятельно посмотреть параметры в Swagger. Разработчикам не приходится искать backend-команду, чтобы выяснить контракт.
Ограничения, конечно, остались: нужно читать документацию, полностью описывать спецификацию и иногда обсуждать с дизайнерами расширение шаблонов. Но это честный обмен — мы один раз потратили инженерное время на создание системы и теперь экономим его на каждой следующей нотификации.
В конце концов, автоматизация рутины — не обязательно про то, чтобы заставить AI писать за нас код. Иногда гораздо полезнее один раз хорошо описать правила, а потом заставить машину им следовать. Освободившееся время можно потратить на куда более интересные задачи — например, на те самые таинственные баги, о которых хочется рассказывать на митапах.
Практический чек-лист
Хотите понять, подходит ли вашей системе spec-driven подход? Задайте себе несколько вопросов.
Много ли однотипных артефактов?
Если добавление нового элемента превращается в копирование десятка похожих файлов — это верный кандидат на генерацию.Что действительно уникально?
Отделите бизнес-логику от boilerplate. Генерировать стоит именно повторяющуюся часть.Есть ли единый контракт?
Параметры разбросаны по коду или живут в головах коллег? Время вынести их в формальную спецификацию.Можно ли генерировать из контракта?
OpenAPI описывает не только API — на его основе отлично создаются типы и код.Какие правила интерфейса повторяются?
Если большинство писем собирается из одних блоков, проверьте: возможно, хватит нескольких шаблонов.Где ловить ошибки?
Изменение контракта может сломать потребителя. Лучше перенести проверку на этап сборки, чем надеяться на ручное тестирование.Что делать с исключениями?
Не гонитесь за 100%. У нас четыре шаблона закрывают 90% кейсов, а для остальных остаётся возможность написать уникальный код.
А как вы используете кодогенерацию в своих проектах? Генерируете что-то из OpenAPI или пошли другим путём?

