Monorepo удобен, когда Next.js, NestJS и общие контракты развиваются вместе. Изменение API проходит одним коммитом: схема, backend, клиент и тесты остаются согласованными.
Но просто сложить проекты в один репозиторий недостаточно. Нужны границы пакетов, детерминированная установка и понятный запуск задач. Их дают pnpm workspaces и Turborepo.
Структура
apps/
web/
package.json
next.config.js
api/
package.json
src/
packages/
contracts/
package.json
src/
config/
package.json
package.json
pnpm-workspace.yaml
pnpm-lock.yaml
turbo.json
apps/web - Next.js, apps/api - NestJS или Fastify, packages/contracts - общие DTO и схемы, packages/config - настройки TypeScript, ESLint и тестов.
Пакет не обязан быть библиотекой для публикации в npm. Это рабочая единица со своими зависимостями и скриптами.
pnpm workspaces
pnpm-workspace.yaml перечисляет расположение проектов:
packages:
- "apps/*"
- "packages/*"
Корневой package.json описывает только общие инструменты:
{
"name": "marketplace",
"private": true,
"packageManager": "pnpm@10.15.0",
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^6.0.0"
}
}
Зависимости приложения объявляют в его собственном package.json. Не складывайте всё в корень: иначе границы пакетов и причины обновлений теряются.
Связь приложений через workspace
apps/api/package.json:
{
"name": "@marketplace/api",
"private": true,
"scripts": {
"build": "tsc -b",
"test": "jest"
},
"dependencies": {
"@marketplace/contracts": "workspace:*",
"@nestjs/common": "^11.0.0"
}
}
apps/web/package.json получает ту же версию контрактов:
{
"name": "@marketplace/web",
"private": true,
"scripts": {
"build": "next build",
"test": "vitest run"
},
"dependencies": {
"@marketplace/contracts": "workspace:*",
"next": "^16.0.0"
}
}
workspace:* означает локальную ссылку внутри репозитория. При публикации пакета её нужно заменить обычной semver-версией, но для внутреннего приложения это не требуется.
Общие контракты
В packages/contracts хранят типы и схемы, которые одинаково понимают frontend и backend:
import { z } from "zod";
export const createProductSchema = z.object({
name: z.string().trim().min(1).max(300),
price: z.number().positive(),
});
export type CreateProductInput = z.infer<typeof createProductSchema>;
export interface Product {
id: number;
name: string;
price: number;
}
Backend использует схему для валидации, frontend - для формы и типов ответа. Не помещайте сюда NestJS, React или доступ к базе: пакет должен оставаться нейтральным к платформе.
Изменение контракта всё равно требует совместимости. Для публичного API сначала добавляют новое поле как необязательное, затем обновляют потребителей, и только потом убирают старое.
Задачи Turborepo
turbo.json описывает зависимости задач и их результаты:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}
build сначала собирает зависимости пакета. Поэтому apps/web получает актуальный contracts/dist до собственной сборки. test и lint выполняются параллельно, если граф пакетов это позволяет.
Запуск всех задач:
pnpm build
pnpm test
pnpm lint
Запуск только нужного пакета:
pnpm turbo run build --filter=@marketplace/api
pnpm turbo run test --filter=@marketplace/contracts
Фильтр по зависимостям:
pnpm turbo run build --filter=@marketplace/web...
Не запускайте npm install или pnpm install отдельно внутри каждого приложения. Установка и lockfile должны быть общими для workspace.
Фильтр изменённых проектов
В CI не всегда нужно собирать всё репозиторие. Turborepo сравнивает изменения с базовой веткой:
pnpm turbo run build test --filter='...[origin/main]'
Такой фильтр выбирает изменённые пакеты и их зависимых. Полную проверку оставляют для релизной ветки или ночного прогона, потому что незатронутый пакет может скрывать интеграционную проблему.
Для корректного сравнения CI получает историю и базовую ветку целиком. Слишком мелкий fetch-depth ломает определение изменений.
Кеширование
Turborepo кеширует результат задачи по её входам: исходникам, зависимостям, конфигурации и переменным окружения. Если входы не изменились, пакет берётся из кеша вместо повторной сборки.
Локальный кеш находится в .turbo. Remote cache позволяет использовать результаты между разработчиками и CI-агентами. Для него задайте URL, команду и токен через переменные окружения CI, не в репозитории.
Ключевые правила:
- кеш не заменяет lockfile;
- секреты не записывают в логи и артефакты задач;
buildперечисляет только реальные выходные файлы;- тестовый PostgreSQL, Redis и другие сервисы изолированы для каждого прогона;
- кеш не должен скрывать изменение миграций или схемы контракта.
CI
Минимальный порядок:
corepack enable
pnpm install --frozen-lockfile
pnpm turbo run lint test build --filter='...[origin/main]'
--frozen-lockfile запрещает менять зависимости молча. Если package.json и pnpm-lock.yaml разошлись, установка падает и заставляет обновить lockfile явно.
Образ для конкретного приложения собирают после проверок. В него включают production-зависимости только нужного пакета и его workspace-зависимостей, а не весь monorepo без разбора.
Типичные ошибки
- Все зависимости объявлены в корне, поэтому приложения получают лишние пакеты и конфликтующие версии.
turbo run buildзапускается безdependsOn, и frontend собирается со старыми контрактами.- CI работает с неполной историей, поэтому фильтр изменений выбирает неверный набор пакетов.
- Тесты читают production-конфигурацию и секреты из общего
.env. - Сборочный образ содержит весь monorepo и devDependencies.
Коротко
- pnpm workspaces управляет приложениями и пакетами через один lockfile.
workspace:*связывает локальные зависимости без публикации в npm.- Общие контракты остаются независимыми от Next.js и NestJS.
dependsOnстроит граф задач, а outputs определяет кешируемые результаты.- CI использует frozen lockfile, фильтры изменений и изолированные тестовые сервисы.
Что почитать дальше
- CI для Node.js — npm, TypeScript, Jest и Testcontainers.
- Принципы конвейера — кеш, стадии и артефакты.
- Next.js production — приложение web из monorepo.