Monorepo Architecture Guide: Nx, Turborepo, Structure & CI/CD
Practitioner guide. Config examples target Turborepo 2.x and Nx 21+ (June 2026). Verify flags on turbo.build and nx.dev before upgrading.
At a Glance - Monorepo Architecture
- Use a monorepo when: 2+ apps share UI, types, or API clients · coordinated releases matter
- Skip it when: unrelated client repos · strict access walls · tiny solo projects
- Pick Turborepo: JS/TS focus, fast setup, Vercel remote cache - best default for Next.js teams
- Pick Nx: Large teams, generators, enforced boundaries, polyglot or Angular-heavy stacks
- Must-haves:
apps/+packages/· dependency rules · affected CI · remote cache - Related: TypeScript · Next.js performance · microservices
What Is Monorepo Architecture?
Monorepo architecture stores multiple applications and shared libraries in one Git repository, with tooling that understands the dependency graph between projects. A polyrepo keeps each app in its own repo - simpler permissions, but shared code becomes npm packages, git submodules, or copy-paste.
Modern monorepos are not “one giant folder.” They use workspaces (pnpm/npm/yarn) plus an orchestrator (Turborepo or Nx) to run builds in topological order, cache task outputs, and skip work that did not change.
my-monorepo/
├── apps/
│ ├── web/ # Next.js storefront
│ ├── admin/ # React admin
│ └── mobile/ # React Native (optional)
├── packages/
│ ├── ui/ # Shared components
│ ├── api-client/ # Typed API SDK
│ ├── config-eslint/ # Shared ESLint config
│ └── config-typescript/
├── turbo.json # or nx.json
├── pnpm-workspace.yaml
└── package.json
When a Monorepo Is the Wrong Choice
Teams adopt monorepos because Google does - then regret the operational overhead. Do not merge repos if:
| Situation | Why polyrepo wins |
|---|---|
| Agency with isolated client projects | No shared code; separate billing, access, and release cadence |
| Hard compliance / IP walls | Repo access must differ by team; monorepo visibility is all-or-nothing unless you split anyway |
| Unrelated tech stacks with no shared tooling | Java ERP + Python ML + mobile game - use polyrepo or meta-repo (git submodules), not one JS monorepo |
| Single app, 1–2 developers | Workspace + Turborepo adds ceremony without payoff; a flat Next.js app is fine |
| Teams that never share code | You pay clone size and CI complexity for zero reuse - fix coordination first |
Good fit: product suite (web + admin + mobile), design system consumed by multiple apps, or micro-frontends sharing types with a Node API. Monorepo ≠ microservices - you can run many services from one repo or many repos; see microservices patterns.
Practical Repository Structure
Convention used by create-turbo and most Nx
workspaces:
| Path | Contains | Import rule |
|---|---|---|
apps/* |
Deployable apps (Next.js, Expo, NestJS) | May import packages/* only |
packages/* |
Libraries (ui, utils, api-client, config) | May import other packages; never apps |
tools/ (optional) |
Scripts, codegen, local CLIs | Dev-only |
infra/ (optional) |
Terraform, Docker compose | Not imported by app runtime code |
Example - multi-app product (e-commerce)
spices-platform/
├── apps/
│ ├── storefront/ # Next.js - customer site
│ ├── vendor-portal/ # Next.js - sellers
│ └── admin/ # Internal ops
├── packages/
│ ├── ui/
│ ├── api-client/ # Shared fetch + Zod types
│ ├── database/ # Prisma schema (if shared)
│ └── tsconfig/
├── turbo.json
└── pnpm-workspace.yaml
One atomic commit can update the API client types and fix all three apps - that is the core value of monorepo architecture.
Turborepo vs Nx
| Criteria | Turborepo | Nx |
|---|---|---|
| Best for | Next.js / React / Node monorepos | Enterprise, Angular, multi-framework |
| Learning curve | Low - mostly turbo.json |
Medium - plugins, project graph, generators |
| Caching | Local + Vercel Remote Cache | Local + Nx Cloud |
| Affected commands | --filter=...[origin/main] |
nx affected -t build |
| Boundary enforcement | ESLint import rules (manual) | Built-in @nx/enforce-module-boundaries |
| Can combine? | Yes - Nx can use Turborepo as task runner; pick one orchestrator to start | |
Default recommendation (2026): New TypeScript product team → Turborepo + pnpm. Team of 10+ with strict architecture rules or Angular → evaluate Nx. Compare at monorepo.tools.
Turborepo Setup
1. Scaffold
pnpm dlx create-turbo@latest my-monorepo
cd my-monorepo
pnpm install
2. Workspace definition
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
3. Task pipeline (Turborepo 2.x)
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
^build means “build my dependencies first” - the key
topological guarantee in monorepo best practices.
4. Wire a shared package
// apps/storefront/package.json
{
"name": "storefront",
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/api-client": "workspace:*",
"next": "^15.0.0"
}
}
5. Run tasks
pnpm turbo build # all packages + apps
pnpm turbo dev --filter=storefront # one app
pnpm turbo lint test --filter=@repo/ui
Nx Monorepo Tutorial
1. Create workspace
npx create-nx-workspace@latest my-org --preset=react-monorepo
cd my-org
2. Project graph & targets
Each app/lib has a project.json (or package.json with
nx config). Nx infers dependencies from TypeScript imports and package.json links.
npx nx graph # visual dependency graph
npx nx show project storefront # targets: build, serve, test
3. Generate a library
npx nx g @nx/react:lib ui --directory=packages/ui --unitTestRunner=vitest
npx nx g @nx/react:app admin --directory=apps/admin
4. Run builds
npx nx run-many -t build # everything
npx nx build storefront # single project
npx nx affected -t build test # only impacted by git changes
Dependency Rules
Without rules, developers import across apps and the graph becomes spaghetti. Enforce:
- apps → packages allowed
- packages → packages allowed (watch cycles)
- apps → apps forbidden
- packages → apps forbidden
Nx - enforce-module-boundaries
// nx.json (excerpt)
{
"pluginsConfig": {
"@nx/js": {
"analyzeSourceFiles": true
}
}
}
// .eslintrc.json - @nx/enforce-module-boundaries
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"allow": [],
"depConstraints": [
{ "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:ui", "type:util", "type:data"] },
{ "sourceTag": "type:ui", "onlyDependOnLibsWithTags": ["type:util"] }
]
}
]
}
}
Turborepo - ESLint import boundaries
// eslint.config.js (eslint-plugin-boundaries or no-restricted-imports)
{
rules: {
"no-restricted-imports": ["error", {
"patterns": [{
"group": ["**/apps/*/**"],
"message": "Do not import from another app. Use packages/."
}]
}]
}
}
Affected Builds
Full monorepo CI on every PR wastes minutes. Affected runs tasks only on projects changed since a base branch (plus their dependents).
| Tool | Command | When to use |
|---|---|---|
| Turborepo | turbo run build test lint --filter=...[origin/main] |
PR CI against main |
| Turborepo (last commit) | turbo run build --filter=...[HEAD^1] |
Pre-push local check |
| Nx | nx affected -t build,test,lint --base=origin/main |
Same - Nx computes graph from file changes |
Tip: Changing
packages/ui should rebuild every app that depends on it - both tools walk the graph upward. Run
turbo run build --dry-run or nx print-affected to debug before tuning CI.
Related reading: Type safety in shared packages: TypeScript best practices · Frontend perf: Next.js performance · Pipeline gates: DevOps & CI/CD Sri Lanka.
CI/CD & Remote Caching
Local Turborepo/Nx caches live in .turbo/ or
.nx/cache. CI without remote cache rebuilds from scratch every run - enable team-wide cache:
- Turborepo:
npx turbo login+turbo link(Vercel) - setTURBO_TOKENandTURBO_TEAMin CI - Nx: Nx Cloud -
nx connect- distributes cache and optional task distribution
GitHub Actions - Turborepo (affected + cache)
name: CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build & test (affected)
run: pnpm turbo run build test lint --filter=...[origin/${{ github.base_ref }}]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
GitHub Actions - Nx affected
- run: pnpm install --frozen-lockfile
- run: npx nx affected -t build,test,lint --base=origin/main --head=HEAD
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
Cache layers (stack them)
| Layer | What it caches |
|---|---|
| pnpm store / node cache | Dependency install |
| Turbo / Nx remote cache | Task outputs (build, test) keyed by inputs |
| Affected filtering | Skips entire projects unchanged |
Monorepo Best Practices
- One package manager - pnpm recommended; commit lockfile; use
workspace:*for internal deps - Shared TypeScript config -
@repo/typescript-configextended per app; see TypeScript best practices - Root scripts only orchestrate -
turbo run/nx run-many; avoid business logic in root package.json - Changesets - if publishing packages to npm, version with @changesets/cli
- CODEOWNERS -
packages/ui/→ design team reviews on every PR - Keep binaries out of Git - Git LFS for assets; never commit
node_modulesor.next - Measure cache hits -
turbo run build --summarizeor Nx Cloud analytics
Monorepo setup and CI that scales
Hashtag Coders - Turborepo/Nx architecture, dependency boundaries, and affected-build pipelines for multi-app products.
FAQ
Monorepo vs polyrepo - quick decision?
Shared code + coordinated releases → monorepo. Independent
products or clients → polyrepo. When unsure, start polyrepo and extract packages/ui when duplication
hurts - migrating two repos into one is easier than splitting a tangled monorepo.
Turborepo vs Nx for a Next.js team?
Turborepo: less config, excellent Next.js/Vercel path. Nx: better if you need generators, strict tags, or Angular alongside React. Both support affected builds and remote cache.
Does a monorepo replace microservices?
No. You can deploy five services from one repo or five repos. Monorepo solves code sharing and atomic changes; microservices solve runtime isolation.
How large is too large?
Pain usually comes from missing boundaries and uncached CI, not file count. Fix dependency rules and affected pipelines before splitting. Google-scale teams use custom tooling; most Sri Lankan product teams are nowhere near that ceiling.
Can Hashtag Coders help set up a monorepo?
Yes - we use monorepos for multi-app client products (Next.js + shared UI packages). See TypeScript best practices for shared-package typing patterns.
Summary
Monorepo architecture pays off
when apps genuinely share libraries and you invest in structure: apps/ + packages/,
dependency rules, affected CI, and remote caching. Choose Turborepo for a fast
Nx monorepo tutorial alternative on smaller TS stacks; choose Nx when enforcement and scale matter. Skip the pattern when projects are unrelated -
that is the most common mistake.