Антон Ларичев

Введение
Монорепозиторий на Turborepo позволяет объединить фронтенд и бэкенд в одном репозитории, сохраняя при этом четкое разделение ответственности и высокую скорость сборки. Если вы работаете с React и NestJS, такая организация проекта ускоряет разработку за счет переиспользования типов, утилит и конфигураций между приложениями.
В этой статье разберем, как организовать фронтенд и бэкенд в одном репо с помощью Turborepo, настроить workspace, общие пакеты и pipeline задач. Покажем реальную структуру проекта, которую можно сразу применить в продакшне.
Почему монорепозиторий на Turborepo, а не мультирепо
Когда фронтенд и бэкенд живут в разных репозиториях, появляются типичные проблемы: дублирование типов, рассинхронизация API-контрактов, сложность локальной разработки. Монорепозиторий решает их, но без правильного инструмента превращается в медленный монолит.
Turborepo решает проблему производительности через:
- Инкрементальные сборки — пересобираются только измененные пакеты
- Кэширование результатов — повторные сборки завершаются мгновенно
- Параллельное выполнение — независимые задачи запускаются одновременно
- Pipeline задач — turbo.json описывает зависимости между задачами
В отличие от Nx, Turborepo не требует плагинов и сложной конфигурации. Вы описываете задачи в turbo.json и продолжаете использовать привычные скрипты package.json.
Структура монорепозитория: apps и packages
Стандартная структура Turborepo проекта разделяет код на два каталога:
monorepo/
├── apps/
│ ├── web/ # Next.js фронтенд
│ └── api/ # NestJS бэкенд
├── packages/
│ ├── shared-types/ # Общие TypeScript типы
│ ├── ui/ # UI-компоненты (React)
│ ├── config-eslint/ # Общая конфигурация ESLint
│ └── config-typescript/# Общий tsconfig
├── turbo.json
├── package.json
├── pnpm-workspace.yaml
└── .gitignore
Каталог apps/ содержит приложения — это то, что деплоится. Каталог packages/ содержит общие библиотеки, которые используются приложениями как зависимости.
Как настроить Turborepo с pnpm с нуля
Начнем с инициализации проекта. Используем pnpm как пакетный менеджер — он лучше всего работает с workspace:
# Создаем проект
npx create-turbo@latest my-monorepo
cd my-monorepo
Если настраиваете вручную, создайте корневой package.json:
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "^2.0.0"
},
"packageManager": "pnpm@9.0.0"
}
И pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"
Настройка turbo.json для фронтенда и бэкенда
Файл turbo.json описывает pipeline — какие задачи запускать и как они зависят друг от друга:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"]
}
}
}
Символ ^ в dependsOn означает: сначала собери зависимости пакета, потом сам пакет. Это гарантирует, что shared-types соберется раньше, чем web или api.
Общие типы между фронтендом и бэкендом
Главное преимущество монорепозитория — единый источник правды для типов. Создадим пакет shared-types:
// packages/shared-types/src/index.ts
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user';
createdAt: string;
}
export interface ApiResponse<T> {
data: T;
message: string;
statusCode: number;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
total: number;
page: number;
limit: number;
}
Теперь и фронтенд, и бэкенд используют одни и те же интерфейсы:
// apps/api/src/users/users.controller.ts
import { User, ApiResponse } from '@monorepo/shared-types';
@Controller('users')
export class UsersController {
@Get(':id')
async getUser(@Param('id') id: string): Promise<ApiResponse<User>> {
const user = await this.usersService.findById(id);
return { data: user, message: 'OK', statusCode: 200 };
}
}
// apps/web/src/api/users.ts
import { User, ApiResponse } from '@monorepo/shared-types';
export async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const json: ApiResponse<User> = await res.json();
return json.data;
}
Если кто-то изменит интерфейс User, TypeScript сразу покажет ошибки и во фронтенде, и в бэкенде. Никакой рассинхронизации.
Локальная разработка: запуск фронта и бэка одной командой
Одна из самых удобных возможностей Turborepo — запуск всех приложений одной командой:
# Запустить все приложения в dev-режиме
pnpm dev
# Запустить только фронтенд
pnpm dev --filter=web
# Запустить фронтенд и его зависимости
pnpm dev --filter=web...
Флаг --filter позволяет работать с конкретным приложением. Три точки (...) означают: запусти пакет и все его зависимости. Это удобно, когда вы меняете общие типы и хотите видеть результат во фронтенде.
Частые ошибки при организации монорепозитория
Слишком много общих пакетов. Не выносите в отдельный пакет код, который использует только одно приложение. Начните с shared-types и добавляйте пакеты по мере необходимости.
Циклические зависимости. Если пакет A зависит от B, а B от A, Turborepo не сможет определить порядок сборки. Решение — вынести общий код в третий пакет.
Отсутствие outputs в turbo.json. Без указания outputs кэширование не работает. Всегда прописывайте, какие файлы создает задача: dist/** для библиотек, .next/** для Next.js.
Тяжелый корневой package.json. Не устанавливайте зависимости приложений в корень. Каждое приложение должно объявлять свои зависимости в собственном package.json.
Заключение
Монорепозиторий на Turborepo — это практичный способ организовать фронтенд и бэкенд в одном репозитории без потери скорости сборки. Вы получаете единые типы, удобную локальную разработку и автоматическое кэширование. Начните с простой структуры — apps/ для приложений и packages/ для общего кода — и расширяйте по мере роста проекта. Turborepo не навязывает архитектуру, а ускоряет ту, которую вы выбрали сами.






Комментарии
0