← All articles
BUILD TOOLS Nx: The Full-Featured Monorepo Build System for Type... 2026-02-28 · 4 min read · nx · monorepo · typescript

Nx: The Full-Featured Monorepo Build System for TypeScript

Build Tools 2026-02-28 · 4 min read nx monorepo typescript build-tools code-generators architecture

Nx is a build system for monorepos that goes significantly further than task runners. Beyond caching and parallelization, it offers code generators, static analysis of your project graph, enforced module boundaries, and tools for understanding the impact of changes.

It's heavier than Turborepo but brings capabilities that matter for larger projects and teams.

Nx project graph visualization showing dependencies between packages, apps, and libraries

Nx vs. Turborepo

This is the first question most developers have. The honest answer:

Nx Turborepo
Setup complexity Higher Lower
Configuration More code Simple JSON
Code generators Built-in None
Project graph Static analysis Inferred from npm deps
Affected detection More accurate Git-based
Module boundaries Enforced via lint None
Remote cache Nx Cloud (paid) or self-hosted Vercel (free) or self-hosted
Plugin ecosystem Extensive Minimal
Learning curve Significant Low

Use Nx when:

Use Turborepo when:

Both do task caching well. The difference is everything around it.

Getting Started

# Create new Nx workspace
npx create-nx-workspace@latest my-org

# Add Nx to existing monorepo
npx nx@latest init

Nx will analyze your existing package.json scripts and create initial configuration.

Workspace Structure

my-org/
├── apps/
│   ├── web/           # Applications
│   └── api/
├── libs/
│   ├── ui/            # Shared libraries
│   ├── utils/
│   └── feature-auth/
├── tools/
│   └── generators/    # Custom code generators
├── nx.json            # Nx configuration
└── package.json

Nx distinguishes between apps (deployable) and libs (shared code). This isn't enforced at the filesystem level, but Nx's tooling builds around this distinction.

nx.json Configuration

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["{projectRoot}/dist"],
      "cache": true
    },
    "test": {
      "cache": true,
      "outputs": ["{projectRoot}/coverage"]
    },
    "lint": {
      "cache": true
    }
  },
  "namedInputs": {
    "production": [
      "default",
      "!{projectRoot}/**/*.spec.ts",
      "!{projectRoot}/jest.config.*"
    ]
  },
  "defaultProject": "web"
}

The {projectRoot} token is project-specific (expands to apps/web, libs/ui, etc.). This is how Nx tracks which outputs belong to which project.

Running Tasks

# Run a task for all projects
nx run-many --target=build

# Run for specific projects
nx build web
nx test @myorg/ui

# Run affected projects only (changed since main)
nx affected --target=test
nx affected --target=build --base=main --head=HEAD

# Show the project graph visually
nx graph

The nx affected command

nx affected determines which projects are affected by changes since your base branch. It uses static analysis (importing relationships) in addition to git changes, which is more accurate than pure git-based detection:

# In CI (PR targeting main)
nx affected --target=build,test,lint --base=origin/main --head=HEAD

If web imports from ui and you change ui, both web and ui are affected. If you change api which web doesn't import, only api is tested.

Plugins and Code Generators

Nx plugins provide first-class integration with specific frameworks. Official plugins:

# Install plugins
npm add -D @nx/next      # Next.js
npm add -D @nx/react     # React
npm add -D @nx/express   # Express
npm add -D @nx/node      # Node.js
npm add -D @nx/jest      # Jest
npm add -D @nx/vite      # Vite
npm add -D @nx/playwright # Playwright

Plugins provide generators (scaffolding) and executors (build/test/serve tasks):

# Generate a new Next.js app
nx generate @nx/next:app my-new-app

# Generate a new shared library
nx generate @nx/react:lib feature-user-profile --directory=libs/features

# Generate a component in an existing library
nx generate @nx/react:component Button --project=ui --export

Generated code follows your configured conventions — no copy-pasting from existing projects.

Module Boundary Enforcement

One of Nx's most powerful features: enforcing that certain libraries can only be used by certain other libraries.

Define tags in project.json:

// libs/feature-auth/project.json
{
  "name": "feature-auth",
  "tags": ["scope:auth", "type:feature"]
}
// apps/web/project.json
{
  "name": "web",
  "tags": ["scope:web", "type:app"]
}

Then enforce boundaries via ESLint:

// .eslintrc.json
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "allow": [],
        "depConstraints": [
          {
            "sourceTag": "type:app",
            "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
          },
          {
            "sourceTag": "type:feature",
            "onlyDependOnLibsWithTags": ["type:ui", "type:util"]
          },
          {
            "sourceTag": "scope:auth",
            "onlyDependOnLibsWithTags": ["scope:auth", "scope:shared"]
          }
        ]
      }
    ]
  }
}

This enforces your architecture: apps can import features, UI components, and utils — but features can't import other features, and auth code can't import billing code.

Violations are caught at lint time, not runtime.

Project Graph

nx graph

Opens an interactive visualization of your dependency graph. You can:

This is invaluable for understanding a large monorepo you've inherited.

Custom Generators

Write your own generators for project-specific scaffolding:

nx generate @nx/plugin:generator my-generator --project=tools
// tools/generators/my-generator/generator.ts
import { Tree, formatFiles, generateFiles } from '@nx/devkit';

export async function myGenerator(tree: Tree, options: MyGeneratorSchema) {
  generateFiles(tree, path.join(__dirname, 'files'), options.directory, {
    name: options.name,
    template: '',  // Required for .template file extension stripping
  });

  await formatFiles(tree);
}

Create template files in tools/generators/my-generator/files/ with __name__.ts.template naming conventions.

Remote Caching

Nx Cloud (managed)

nx connect-to-nx-cloud

Free for individuals and small teams. Paid for larger usage. The tightest integration — results appear in the Nx Cloud dashboard.

Self-hosted remote cache

The open-source nx-remote-cache package or a compatible S3/GCS cache:

// nx.json
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint"],
        "remoteCache": {
          "url": "https://my-cache-server.com"
        }
      }
    }
  }
}

Migrating Between Nx Versions

Nx provides automated migration:

nx migrate latest  # Updates nx.json, project.json, and scripts
nx migrate --run-migrations  # Apply migrations

This is significantly better than manually updating Nx — migrations handle breaking changes automatically.

When Nx Is Overkill

Not every monorepo needs Nx. Skip it if:

Turborepo or even plain npm/pnpm workspaces may be enough. Nx shines as the monorepo grows in complexity and team size.


More TypeScript tooling guides at DevTools Guide newsletter.