Software Architecture, Development Tools

Monorepo Architecture Guide: Nx, Turborepo, Structure & CI/CD

27th April, 2026
Updated: 25th June, 2026
12 min read
Software Architecture, Development Tools
MonorepoTurborepoNxSoftware ArchitectureCI/CDpnpmTypeScriptNext.jsDevOpsBuild Tools
HC

Hashtag Coders

Software Engineers & Digital Strategists

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) - set TURBO_TOKEN and TURBO_TEAM in 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-config extended 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_modules or .next
  • Measure cache hits - turbo run build --summarize or Nx Cloud analytics

Monorepo setup and CI that scales

Hashtag Coders - Turborepo/Nx architecture, dependency boundaries, and affected-build pipelines for multi-app products.

Contact Us Web Development TypeScript Guide

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.

Ready to get started?

Turn these insights into real results for your business

Hashtag Coders specialises in delivering exactly the solutions discussed in this article. Let's talk about your project - the first consultation is completely free.

No commitment requiredFree initial consultationServing clients in Sri Lanka & globallyTransparent pricing