Монорепозиторий и Turborepo: когда нужен, а когда лишний
Когда в команде появляется второй проект, первый технический вопрос звучит так: куда положить код? Заводим отдельный репозиторий или храним всё в одном месте? Я успел ответить на этот вопрос с обеих сторон: в отдельных репозиториях уставал от синхронизации версий, а в монорепозитории часами ждал CI из-за неправильно настроенного кэша. В этой статье разберём, когда монорепозиторий приносит реальную пользу, когда он — лишняя сложность, и как практически работать с Turborepo.
Что такое монорепозиторий — и чем он не является
Монорепозиторий — это несколько независимых приложений и пакетов, живущих в одном git-репозитории. Обратите внимание: независимых. apps/web и apps/api собираются отдельно, деплоятся отдельно и могут иметь разные версии. Общее у них только место хранения кода.
Путать это с монолитом — распространённая ошибка. Монолит — решение об архитектуре: вся система работает и деплоится как одно приложение. Монорепозиторий — решение об организации кода. В монорепозитории можно держать пять микросервисов, а в polyrepo — писать монолит: эти две оси друг от друга не зависят.
Когда он приносит пользу
Я три года использую монорепозиторий в рабочих проектах, и польза отчётливо видна в трёх местах.
- Общие типы и утилитарные пакеты. Фронтенд и бэкенд используют один пакет
types. Если контракт API меняется, TypeScript сразу показывает ошибку с обеих сторон — во время компиляции, а не в продакшене. - Атомарные изменения. Новый endpoint и использующий его UI приходят в одном PR. С двумя репозиториями это превращается в два PR, два ревью и танец «сначала мёржим бэкенд, потом фронтенд».
- Один CI и один стандарт. Правила линтера, конфигурация тестов, версии зависимостей — всё в одном месте. Проблема «у нас ESLint 8, а у них 9» исчезает.
Когда он лишний
Монорепозиторий не бесплатен. Настройка тулинга, понимание кэша, защита границ пакетов — всё это требует времени. В следующих случаях эта цена себя не оправдывает:
- Приложение одно. Если общего кода нет, то и делить нечего. Обычного репозитория достаточно.
- Команда совсем маленькая. Когда один-два человека работают над одним проектом, проблемы координации, которую решает монорепозиторий, ещё не существует.
- Границы пакетов размыты. Если вы не знаете, какой код к какому пакету относится, монорепозиторий эту путаницу не решит — он её формализует и умножит. Сначала разберитесь в домене, потом делите.
Основы Turborepo
Turborepo — это оркестратор, который запускает задачи внутри монорепозитория (build, lint, test) в правильном порядке и по возможности из кэша. Вся конфигурация живёт в файле turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**", "test/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}Самая важная строка — "dependsOn": ["^build"]. Символ ^ означает «сначала собери мои зависимости»: если пакет web зависит от ui, то turbo build сначала соберёт ui, а потом web. А test привязан к build без ^ — это значит «запусти тесты после моей собственной сборки».
Локальный кэш — главная магия Turborepo. Для каждой задачи turbo вычисляет хеш: исходные файлы пакета (inputs), хеш зависимостей, используемые переменные окружения и конфигурация задачи. По этому хешу результат сохраняется в папке .turbo. Если в следующий раз хеш не изменился, turbo не запускает сборку — он просто возвращает готовые outputs и логи терминала. Если видите в терминале надпись FULL TURBO — всё пришло из кэша.
Remote cache: главная выгода для команды
Локальный кэш работает только на вашей машине. Remote cache хранит те же хеши на общем сервере — на Vercel или в self-hosted варианте:
npx turbo login
npx turbo linkТеперь если коллега уже собрал пакет ui, то после git pull команда turbo build возьмёт готовый результат с сервера — пересобирать не придётся. В CI это заметно ещё сильнее: в pull request собираются только изменённые пакеты, остальное приходит из кэша. В нашем проекте среднее время CI сократилось примерно вдвое — особенно резкая разница в PR, которые трогают только один маленький пакет.
Практическая структура
Вот типичная структура, которую использую я:
apps/
web/ # Next.js фронтенд
api/ # NestJS бэкенд
packages/
ui/ # общие React-компоненты
config/ # пресеты eslint, tsconfig, prettier
types/ # контракты API, zod-схемыПакеты связываются через протокол workspace::
{
"name": "@acme/web",
"dependencies": {
"@acme/ui": "workspace:*",
"@acme/types": "workspace:*"
}
}Самой структуры недостаточно — нужны правила связей. Мои правила простые:
apps/*зависят от пакетов, но ни один пакет не зависит отapps/*.types— самый нижний слой, он не зависит ни от чего.uiможет зависеть только отtypes.configне экспортирует код, только пресеты.
Если закрепить эти правила ограничениями импорта в ESLint, границы будут жить в коде, а не в документации.
Типичные ловушки
- Циклические зависимости. Если пакет
uiимпортирует изtypes, аtypesпо какой-то причине — изui, turbo не сможет построить граф зависимостей, и сборка сломается. Решение — строгие однонаправленные слои: нижний слой никогда не знает о верхнем. - Болезнь «всё в shared». Каждая новая функция сразу переезжает в общий пакет, и в итоге любое касание
sharedпересобирает половину репозитория. Моё правило: пока код не нужен минимум в двух местах, он остаётся там, где живёт. - Неправильные `inputs`. Это самая опасная ловушка, потому что она молчит. Если сборка зависит от кодогенерации или env-файла, а они не указаны в
inputs, turbo вернёт устаревший кэш — и вы задеплоите старый код. Бывает и обратное: еслиinputsслишком широкие, даже изменение README инвалидирует кэш. Явно указывайте переменные окружения в полеenv, а общие файлы — вglobalDependencies.
Вывод: дерево решений
Коротко — логика, которой пользуюсь сам:
- Одно приложение, общего кода нет — обычный репозиторий, о монорепозитории не думайте.
- Два и больше приложений используют общие типы и компоненты — монорепозиторий + Turborepo, это самый сильный сценарий.
- Приложений много, но общего кода между ними почти нет и команды работают независимо — polyrepo тоже неплохой выбор.
- Выбрали монорепозиторий — с первого дня зафиксируйте границы пакетов и защитите их линтером.
Монорепозиторий — инструмент, а не цель. Он решает проблему координации; если проблемы ещё нет, решение не обязательно.