Автогенерация типов показала, что проблема была не в типах

Каждый день мы натыкаемся на одни и те же грабли в связке фронта и бэка. Каждые такие грабли — это деньги и время. А если ещё и договариваться не умеем, процессов нет и решать никто не берётся, то дальше начинаются конфликты, атмосфера в команде проседает и проблемы копятся ещё быстрее.
Статья не про саму автогенерацию — с ней всё понятно, про неё и без меня написано достаточно. Мы внедрили генерацию типов из OpenAPI, и она вытащила наружу то, что с генерацией напрямую не связано: кто не хочет писать комментарии, почему у одной сущности два имени и почему у нас падал dev. По сути — про команду, эго и умение договариваться.
Сразу оговорюсь: это не единственно верный путь. Команды и компании разные, у всех свои нюансы, я рассказываю, как было у нас. Но если автогенерации у вас ещё нет и вы соберётесь её внедрять — будете хотя бы знать, что вас ждёт.
Признание и проблемы
Все наши проекты мы выполняем своей командой, так сказать, «под крышей»: наш фронт и наш бэкенд. Поэтому все возникающие проблемы, влияющие на общее качество проекта, — в зоне ответственности обеих команд, фронта и бэкенда. И решать их нужно всей командой. Оставить это только на плечах фронта или бэкенда — не наш вариант.
Поэтому первое, с чего мы начали, — это признали, что проблемы общие и их нужно решать вместе ради общего блага. У команды нет варианта сказать «это не наши проблемы, решайте их сами». Конечно, без фанатизма: привлекать фронтов к решению, какую БД использовать, не стоит.
Автогенерация
Концептуально внедрение и результат автогенерации выглядят очень просто:
Бэк как‑то документирует апи и на выходе предоставляет JSON по стандарту OpenAPI
Фронт берёт этот OpenAPI JSON ⇒ генерирует типы ⇒ использует в API‑слое
Что это даёт:
Всегда (почти) актуальное состояние контрактов на бэке и на фронте
Изменения на бэке сразу явно отображаются на фронте: поменялся респонс ⇒ типизация сразу подскажет, где пошли ошибки
Один язык контрактов на уровне апи, одни и те же поля/сущности не имеют нескольких названий
До 30% сокращение рутинной работы, чем больше контракты, тем больше выгода: больше не описываем типы ответов/запросов для API
На бумаге всё выглядит просто и понятно. Но есть ряд проблем, с которыми мы столкнулись и которые не связаны с автогенерацией, но которые важно было решить и которые глобально и качественно влияют на всю разработку и проект. Дальше будем говорить о них и как мы их решали. Если вы ещё автогенерацию не внедрили, то наверняка с ними столкнётесь и у вас будет представление о том, как и решать заранее.
Я не хочу писать комментарии
Наш стек на бэкенде — это PHP + Laravel.
Для автогена на бэке используется библиотека Scramble, чтобы она генерировала доку, нужно описывать PHP‑комментариями контроллеры, ресурсы и запросы. Пока что она не может вывести полностью документацию только из описанных типов в PHP.
И это было первой проблемой, которую нужно было преодолеть.
В отличие, например, от FastAPI, где для доки используется питоновская типизация контроллеров и дтошек на Pydantic, в нашем стеке есть только один вариант — писать комментарии к контроллерам и ресурсам.
Мы сразу столкнулись: «ну вот за ними ещё следить, руками писать, автокомплита там нет», «ну вот комментарии ненадёжные, если там поле string, не факт что оно такое будет», «ну вот, это много повторяющихся комментариев для id, slug и так далее».
Решилось это очень просто, не было выбора 🙂 Какие есть альтернативы? Инструменты генерации доки без комментариев? Ну таких на тот момент (скорее всего, и сейчас) всё ещё нет. Документировать в Postman/Insomnia? а) ресурсоёмко, б) не гибко, в) бесплатные версии не подходят, надо платить деньги, а деньги платить мы не хотим, г) писать доку контрактов руками для Swagger — ну уж нет, тогда лучше комментарии, ближе к коду будут.
Поэтому за неимением альтернатив очень быстро с этим смирились и набили руку, как надо делать.
Как завещал дядюшка Боб: один термин — одно значение
Дядя Боб, Эрик Эванс и другие давно говорят базу, с которой невозможно спорить: язык команды должен совпадать с языком бизнеса, документации и кода. Не должно быть «переводчика» между обсуждением и реализацией.
Казалось бы, идея базовая, но сколько с этим было проблем у нас, в других командах или в кодовой базе, которую мы получали на поддержку: разные названия одного и того же на бэкенде и на фронте, гранулирование и наследование дтошек, следовательно, при изменениях на бэке очень сложно становится спрогнозировать, что же поменяется на фронте, какой объём работ нужен, или когда на фронте услуги это services, а на бэкенде это offerings (чтобы не писать ServicesService), и так далее.
Как мы это решали:
Проектирование и согласование перед началом спринтов: как будем называть сущности, какие будут дто, какие у них зоны ответственности и так далее. Поначалу занимало время, но когда все привыкли, встало на рельсы и больше не занимает много времени.
Если фронт по каким‑либо причинам начинает работу раньше, то также обсуждается, какие контракты и сущности как будут называться
Никаких наследований контрактов и дтошек — за нарушение 100 отжиманий и 100 кругов вокруг офиса
Чтобы на фронте увеличить стабильность компонентов — для компонентов свои модели только с нужными полями и к ним дтошки, чтобы мапить контракты в эту модель, либо просто отдельные модели
Не использовать дто для типизации чего‑либо, кроме API‑слоя: имеется в виду, что исходя из этого пункта и пункта 4 мы не используем API‑дтошки, например, для пропсов компонентов
Опытным работягам многое может показаться базой. Но это не означает, что с этими проблемами молодые команды не столкнутся, и самое главное — это пережить, разобраться, как надо делать, сделать выводы и двигаться дальше. А пока предпосылок для обсуждения этих проблем не было, глобально оно и не решалось. Можно считать это точкой роста команды, и это нормально.
Моё любимое — camelCase
Спор белого и чёрного. Я прекрасно понимаю, что есть какие‑то общие стандарты нотаций, как принято делать, как не стоит делать, и так далее. Но аргумент «мне не нравится camelCase, поэтому мы его не будем использовать» — точно не подходит. Выгода для нас как для фронтов очевидна: мы не пишем маппер одних и тех же полей из snake_case в camelCase, просто используем. Учитывая, что команды под одной крышей, то не воспользоваться этой оптимизацией будет странно. А если тебе не нравится, то: «почему мы должны тратить больше трудозатрат (и денег как следствие), чтобы тебе нравилась нотация?» Вопрос риторический.
Можно было бы выбрать стратегию, что все респонсы с бэка мапятся в camelCase, но: а) это дополнительное время к рантайму, б) хоть и не сложная вещь, но это ещё и обёртку поддерживать, в) диссонанс — так как Scramble создаёт доку по полям классов, то странно, что поле в одной нотации, в доке в другой. Короче, тоже не вариант.
Технически не сложно написать type utilities для TypeScript, чтобы тип snake_case транслировать в camelCase, и есть готовые функи для toCamelCase, грубо говоря. Но я решил так: вместо того чтобы втихую что‑то делать на фронте или бэкенде, лучше подойти и обсудить вариант с camelCase “нативно” руками в дтошках, нежели городить что‑то дополнительное. Это самый лучший вариант, когда команды под крышей. И такие разговоры развивают команду больше обсуждать общие решения. Поэтому так и было сделано.
По итогу ничего страшного не случилось, на бэке дтошки руками пишем поля в camelCase. Всех устраивает, без дополнительных костылей.
Единственная оговорка: на названия колонок это не распространяется.
Прочие изменения в архитектуре и процессах
Кроме хорошего, возникли проблемы, которые повлияли на архитектуру проекта и процессы: переход к монорепе, локально поднимать бэк, изменение воркфлоу и донастройка CI/CD. Давайте по порядку.
Чаще всего фронты просто коннектятся к деву бэка, так и работают. Как выглядит такой воркфлоу по задаче:
ставят задачу
бэкенд что‑то делает
катит в дев
либо отдельной задачей, либо в той же фронт начинает работать
Проблемы:
так как фронту для сборки нужно сгенерировать апи, а потом собрать проект, то первая проблема: поменялось апи — фронт не собрался, типы не сошлись. Сами типы мы в репозитории не храним: это производный артефакт, генерится одной командой из OpenAPI, та же версия бэка даёт тот же результат. Генерим локально при запуске и в пайплайне при сборке. Бонусом не разгребаем конфликты в сгенерированном коде.
следовательно, дев может какое‑то время не работать → нет возможности протестировать задачу со стороны бэка
это тормозит другие задачи и вставляет палки в колёса всем подряд
У нас и так всё в докерах было завёрнуто. Но, как я писал, фронты чаще всего не поднимают проект в Docker (можно долго говорить, как это плохо, но это уже в прошлом). Локально поднимаем и фронт, и бэк в Docker, если надо — накатываем дамп с дева локально.
Но чтобы было ещё удобнее, перешли на монорепозиторий вместо отдельного репозитория для фронта и для бэкенда, чтобы вести одну задачу и не деплоить незаконченные артефакты в дев‑окружение.
Но это ещё не всё, фронт ещё может ломаться после изменений контрактов.
Поэтому разом поменяли CI/CD и воркфлоу:
Поставили задачу
Бэк берёт в работу и создаёт ветку
Как закончит — отдаёт фронту задачу и ветку (монорепа всё‑таки)
Тут ещё задачу никуда не заливали
Фронт делает задачу, как закончит — пушит в дев и отдаёт на тест
Если приходят доработки, то всё просто: если только фронт, значит только фронт; если бэк и бэк поменял контракты, то возвращаемся к пункту 3
Так мы получили прозрачность работы над задачей, дев не падает, фронт всегда работает с актуальными контрактами. Это не исключает ошибки на 100 процентов, но так как мы двигаемся двухнедельными спринтами, это сильно сокращает издержки, ошибки и не блочит остальных.
Всё ещё остаются рутинные проблемы: фронту надо поднимать бэк, периодически сносить и накатывать дампы, для запуска проекта нужно больше ресурсов, нужно чуть лучше менеджерить задачи. Но на данный момент проделанной работы достаточно, чтобы нивелировать эти проблемы улучшенным качеством разработки, процессами, согласованностью и, самое главное, снижением оценок задач (которые влияют на итоговую смету проекта) от 20 до 40 процентов. На практике даже сэкономленные 200–300 тысяч в текущее время влияют на экономику проекта в пользу клиентов и повышают шансы, что клиент согласится вести дела с нами.
Откуда 20–40% и 200–300 тысяч? Полгода после внедрения мы смотрели сметы и трудозатраты и сравнивали с типовыми проектами, которые делали до автогенерации.
20–40% — это про оценки задач в спринте. Спринты у нас двухнедельные, за этот же период выставляются счета, так что экономия сразу видна в деньгах.
200–300 тысяч — столько мы экономим клиенту на первичной смете нового проекта. Дальше работа идёт по спринтам, там суммы меньше, но на дистанции, сами понимаете, накопленная экономия только растёт.
Обратная сторона медали
Без последствий ничего не бывает:
Ревью бэка стало дольше — если вы договорились о контрактах, надо следить, что базар соблюдается
Онбординг новых ребят в фронт — и бэк‑отдел стал дольше:
Надо больше рассказывать про флоу работы
Надо рассказывать, почему было сделано так, а не иначе
Рассказывать, как с этим работать, как мы адаптировали это архитектурно на бэке и на фронте
Какие инструменты в репозитории для этого используем
Какие могут быть проблемы и как их решать
Ответы на все вопросы в нашей документации как бы закрывают это, но не до конца, вопросы всё равно могут оставаться. Но сказать, что это прям дорогая проблема, я не могу, эта нагрузка не исчисляется днями‑неделями.
А что с продакшеном в этом всём деле? А проду уже и неважно, в него попадает стабильная протестированная версия (тест прода руками QA никуда, конечно, не пропал).
Кому это не подойдёт
Я точно не хочу, чтобы вы считали это серебряной пулей. В нашем контексте, который я задал в начале и который актуален для многих аутсорс‑студий и небольших команд, я уверен, это актуально. Но также есть команды и компании, в которых это не приживётся.
По моему мнению, это не подойдёт:
Командам, которые пишут только бэк, а потребитель апи — это клиенты, другие команды и так далее. Тут команда внутри сама договаривается, как ей удобнее вести дела. Либо просто придерживаться общего стандарта на стеке, на котором команда работает.
Возможно, это не подойдёт фулстекам, которые пишут на Inertia.js — дополнительно описывать дтошки, генерировать доку и всё остальное как будто бы оверхед, потому что с Inertia.js это монорепа, и все данные, которые попадают на страницу, можно зайти и посмотреть в контроллере.
Команде фронтов, где бэк — это чужая команда «не под крышей»: проще по классике делать, и если какие‑то неприятности из апишки, то локально закрывать их в слое с апи
Честно скажу — GraphQL я не использовал, но из того, что знаю, там вопрос этот уже из коробки решён архитектурно
Итого
Давайте тезисно зафиналим, что получили в итоге:
прокачали качество фронта и бэкенда
уменьшили трудозатраты на задаче ⇒ уменьшили оценку ⇒ уменьшили стоимость сметы ⇒ ускорили разработку
улучшили процессы ⇒ последовательность работы над задачей ⇒ с монорепой и CI/CD уменьшили количество ошибок и блокеров дев‑окружения
меньше операционной пробуксовки ⇒ меньше багов и непоняток ⇒ клиент больше и чаще доволен
моё любимое: команды продвинулись в навыке командного решения проблем, что также увеличило сплочённость и общую заинтересованность в качественном результате
P. S. Некоторые проблемы могут показаться какими‑то смешными. Но такова реальность, я её не скрываю, мы с ними столкнулись. Вы с ними, может быть, не столкнётесь, не хорошо и не плохо, просто реальность какая она есть, и самое главное, что это удалось решить и прокачать не только хардовые компетенции, но и софтовые.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.