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 подход? Задайте себе несколько вопросов.

  1. Много ли однотипных артефактов?
    Если добавление нового элемента превращается в копирование десятка похожих файлов — это верный кандидат на генерацию.

  2. Что действительно уникально?
    Отделите бизнес-логику от boilerplate. Генерировать стоит именно повторяющуюся часть.

  3. Есть ли единый контракт?
    Параметры разбросаны по коду или живут в головах коллег? Время вынести их в формальную спецификацию.

  4. Можно ли генерировать из контракта?
    OpenAPI описывает не только API — на его основе отлично создаются типы и код.

  5. Какие правила интерфейса повторяются?
    Если большинство писем собирается из одних блоков, проверьте: возможно, хватит нескольких шаблонов.

  6. Где ловить ошибки?
    Изменение контракта может сломать потребителя. Лучше перенести проверку на этап сборки, чем надеяться на ручное тестирование.

  7. Что делать с исключениями?
    Не гонитесь за 100%. У нас четыре шаблона закрывают 90% кейсов, а для остальных остаётся возможность написать уникальный код.

А как вы используете кодогенерацию в своих проектах? Генерируете что-то из OpenAPI или пошли другим путём?