Back to Blog

TypeScript Monorepo with Nx: Project Graph, Affected Builds, and Remote Caching

Build a TypeScript monorepo with Nx. Project graph for dependency tracking, affected commands to run only what changed, remote caching with Nx Cloud, and generator automation.

Viprasol Tech Team
July 22, 2026
12 min read

TypeScript Monorepo with Nx: Project Graph, Affected Builds, and Remote Caching

Monorepos let teams share code without the overhead of publishing packages, keep all changes in one PR, and enforce consistent tooling. The problem is build time. When your repo has 50 projects and CI runs every test, lint, and build on every commit, CI becomes the bottleneck.

Nx solves this with three mechanisms: project graph (knows what depends on what), affected analysis (runs only what a commit actually touched), and remote caching (never rebuild what's already been built). The result: CI that runs in minutes instead of hours, even as the repo grows.


Nx vs. Turborepo vs. Lerna

ToolProject GraphRemote CacheGeneratorsNx CloudBest For
Nxโœ… Fullโœ… Nx Cloud or customโœ… Built-inโœ… First-classFull-stack, complex repos
Turborepoโœ…โœ… Vercel Remote CacheโŒVia VercelFrontend-heavy repos
Lernaโœ… (basic)โŒโŒโŒPublishing npm packages
pnpm workspacesโŒโŒโŒโŒSimple package sharing

Choose Nx when you have full-stack TypeScript (Next.js + Node.js + shared libs), need code generators, or want to enforce architectural boundaries. Choose Turborepo for pure frontend monorepos deployed on Vercel.


Workspace Setup

npx create-nx-workspace@latest myapp \
  --preset=ts \
  --packageManager=pnpm \
  --nxCloud=yes
myapp/
  apps/
    web/          โ† Next.js frontend
    api/          โ† Fastify API
    admin/        โ† Next.js admin panel
  libs/
    ui/           โ† Shared React components
    utils/        โ† Shared TypeScript utilities
    types/        โ† Shared TypeScript types
    db/           โ† Database client + migrations
    auth/         โ† Auth logic (shared by web + api)
  nx.json         โ† Nx configuration
  package.json    โ† Root package.json (pnpm workspaces)
  tsconfig.base.json

nx.json: Core Configuration

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "nxCloudAccessToken": "${NX_CLOUD_ACCESS_TOKEN}",
  "defaultBase": "main",

  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/*.spec.ts",
      "!{projectRoot}/**/*.test.ts",
      "!{projectRoot}/jest.config.*",
      "!{projectRoot}/.eslintrc.json"
    ],
    "sharedGlobals": [
      "{workspaceRoot}/tsconfig.base.json",
      "{workspaceRoot}/package.json"
    ]
  },

  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],      // Build deps first
      "inputs": ["production", "^production"],
      "cache": true                  // Cache build outputs
    },
    "test": {
      "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    },
    "e2e": {
      "cache": false                // Never cache E2E โ€” always run fresh
    }
  },

  "generators": {
    "@nx/react": {
      "library": { "unitTestRunner": "vitest", "style": "tailwind" }
    },
    "@nx/node": {
      "library": { "unitTestRunner": "vitest" }
    }
  }
}

tsconfig.base.json: Path Aliases

{
  "compileOptions": {
    "baseUrl": ".",
    "paths": {
      "@myapp/ui":       ["libs/ui/src/index.ts"],
      "@myapp/utils":    ["libs/utils/src/index.ts"],
      "@myapp/types":    ["libs/types/src/index.ts"],
      "@myapp/db":       ["libs/db/src/index.ts"],
      "@myapp/auth":     ["libs/auth/src/index.ts"]
    }
  }
}

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Project Configuration

Each app and lib has a project.json:

// apps/api/project.json
{
  "name": "api",
  "projectType": "application",
  "sourceRoot": "apps/api/src",
  "targets": {
    "build": {
      "executor": "@nx/esbuild:esbuild",
      "options": {
        "outputPath": "dist/apps/api",
        "main": "apps/api/src/main.ts",
        "tsConfig": "apps/api/tsconfig.app.json",
        "bundle": true,
        "platform": "node",
        "format": ["cjs"],
        "external": ["pg-native"]
      }
    },
    "serve": {
      "executor": "@nx/js:node",
      "options": {
        "buildTarget": "api:build",
        "watch": true
      }
    },
    "test": {
      "executor": "@nx/vite:test",
      "options": {
        "configFile": "apps/api/vite.config.ts"
      }
    },
    "docker-build": {
      "executor": "nx:run-commands",
      "dependsOn": ["build"],
      "options": {
        "command": "docker build -f apps/api/Dockerfile -t myapp-api:{args.tag} dist/apps/api"
      }
    }
  },
  "tags": ["scope:backend", "type:app"]
}
// libs/auth/project.json
{
  "name": "auth",
  "projectType": "library",
  "sourceRoot": "libs/auth/src",
  "targets": {
    "build": {
      "executor": "@nx/js:tsc",
      "options": {
        "outputPath": "dist/libs/auth",
        "main": "libs/auth/src/index.ts",
        "tsConfig": "libs/auth/tsconfig.lib.json"
      }
    },
    "test": {
      "executor": "@nx/vite:test"
    },
    "lint": {
      "executor": "@nx/eslint:lint"
    }
  },
  "tags": ["scope:shared", "type:lib"]
}

Affected Analysis: The Key to Fast CI

# Run tests only for projects affected by changes since main branch
npx nx affected --target=test --base=main --head=HEAD

# Build only affected apps
npx nx affected --target=build --base=main --head=HEAD

# Lint only affected projects
npx nx affected --target=lint --base=main

# See what would be affected without running
npx nx affected:graph --base=main

How it works: Nx builds a project graph from import relationships. When libs/auth/src/jwt.ts changes, Nx knows that apps/api and apps/web both import @myapp/auth โ€” so it marks them as affected and runs their tests too. Unchanged projects skip entirely.


๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

GitHub Actions CI with Nx Affected

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}

jobs:
  main:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        ports: ['5432:5432']
        options: --health-cmd pg_isready --health-interval 5s

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # Full history needed for affected analysis

      # Set base for affected โ€” main branch for PRs, previous commit for push
      - name: Derive SHAs for nx affected
        uses: nrwl/nx-set-shas@v4
        # Sets NX_BASE and NX_HEAD env vars

      - uses: pnpm/action-setup@v3
        with: { version: 9 }

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      - name: Lint affected
        run: pnpm nx affected --target=lint --base=$NX_BASE --head=$NX_HEAD --parallel=4

      - name: Test affected
        run: pnpm nx affected --target=test --base=$NX_BASE --head=$NX_HEAD --parallel=4
        env:
          DATABASE_URL: postgres://test:test@localhost:5432/testdb

      - name: Build affected
        run: pnpm nx affected --target=build --base=$NX_BASE --head=$NX_HEAD --parallel=2

  # E2E only on main branch push (expensive)
  e2e:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    needs: main
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - run: pnpm install --frozen-lockfile
      - run: pnpm nx affected --target=e2e --base=HEAD~1 --head=HEAD

Remote Caching: Skip Already-Built Work

With Nx Cloud, build artifacts are stored remotely. If another developer or CI run already built libs/auth at that exact git tree state, your run downloads the cache instead of rebuilding.

# Local: first run builds and caches
pnpm nx build auth
# [nx] Cache miss - building auth...
# [nx] โœ“ auth built in 4.2s โ€” cached to Nx Cloud

# Second run (same inputs): instant
pnpm nx build auth
# [nx] Cache hit โ€” skipping auth build (restored from Nx Cloud)
# [nx] โœ“ auth in 0.1s

For CI: if a PR only changes apps/web, libs/auth and apps/api builds are served from cache. CI completes in 2โ€“3 minutes instead of 12 minutes.

Self-hosted cache alternative (no Nx Cloud subscription):

// nx.json โ€” use S3 as remote cache backend
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx-remotecache-s3",
      "options": {
        "bucket": "myapp-nx-cache",
        "region": "us-east-1"
      }
    }
  }
}

Code Generators: Automate Project Creation

# Generate a new React library
pnpm nx generate @nx/react:library ui-forms \
  --directory=libs/ui-forms \
  --unitTestRunner=vitest \
  --style=tailwind \
  --publishable \
  --importPath=@myapp/ui-forms

# Generate a new API endpoint (custom generator)
pnpm nx generate @myapp/tools:api-route \
  --name=webhook \
  --project=api

Custom Generator

// tools/generators/api-route/index.ts
import {
  Tree,
  generateFiles,
  names,
  joinPathFragments,
  formatFiles,
} from '@nx/devkit';

interface Schema {
  name: string;
  project: string;
}

export default async function (tree: Tree, schema: Schema) {
  const projectRoot = `apps/${schema.project}/src/routes`;
  const routeNames = names(schema.name);

  // Generate route file from template
  generateFiles(
    tree,
    joinPathFragments(__dirname, 'files'),  // Template directory
    projectRoot,
    {
      ...routeNames,
      tmpl: '',
    },
  );

  await formatFiles(tree);
}
// tools/generators/api-route/files/__name__.route.ts__tmpl__
// Auto-generated route: <%= name %>
import type { FastifyInstance } from 'fastify';

export async function <%= camelCase %>Routes(app: FastifyInstance): Promise<void> {
  app.get('/<%= dasherize %>', async (_req, reply) => {
    return reply.send({ message: 'Hello from <%= name %>' });
  });
}

Enforcing Architectural Boundaries

Nx's module boundary rules prevent spaghetti imports:

// .eslintrc.json (root)
{
  "plugins": ["@nx"],
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "enforceBuildableLibDependency": true,
        "allow": [],
        "depConstraints": [
          // Apps can import from libs, but not from other apps
          {
            "sourceTag": "type:app",
            "onlyDependOnLibsWithTags": ["type:lib", "type:util"]
          },
          // Backend libs cannot import from frontend libs
          {
            "sourceTag": "scope:backend",
            "notDependOnLibsWithTags": ["scope:frontend"]
          },
          // Shared libs cannot import app-specific code
          {
            "sourceTag": "scope:shared",
            "notDependOnLibsWithTags": ["scope:backend", "scope:frontend"]
          }
        ]
      }
    ]
  }
}

Performance: Nx vs. Running Everything

Repo SizeRun EverythingNx Affected (cold)Nx Affected (cache hit)
5 projects3 min2 min30 sec
20 projects12 min4 min1 min
50 projects35 min6 min2 min
100 projects80 min8 min3 min

Estimates for test + lint + build on a 4-core CI runner. Remote cache hit assumes another runner already completed the same work.


Working With Viprasol

Our team sets up and migrates TypeScript projects to Nx monorepos โ€” from workspace configuration through custom generators and CI optimization.

What we deliver:

  • Nx workspace setup (apps + libs structure, tsconfig path aliases)
  • GitHub Actions CI with nx affected and Nx Cloud integration
  • Module boundary rules enforcement
  • Custom code generators for your team's patterns (service, route, component)
  • Migration from existing multi-repo or unstructured monorepo

โ†’ Discuss your monorepo setup โ†’ Software development services


Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.