Developer tools

Project Scaffolding in 2026: Monorepo, Turborepo and Nx

Structuring new projects in 2026 requires more than `npm init`. Monorepos with Turborepo or Nx offer shared configs, build caching and consistency across multiple packages.

3/12/20269 min readDev tools
Project Scaffolding in 2026: Monorepo, Turborepo and Nx

Executive summary

Structuring new projects in 2026 requires more than `npm init`. Monorepos with Turborepo or Nx offer shared configs, build caching and consistency across multiple packages.

Last updated: 3/12/2026

Introduction: The evolution of scaffolding

In 2026, npm init, create-react-app and vue-cli are no longer sufficient for professional projects. Teams require:

  • Monorepo for shared configs (ESLint, Prettier, TypeScript)
  • Build caching for fast development (rebuilds in seconds, not minutes)
  • Shared packages for reuse (UI components, utilities, types)
  • Intelligent CI/CD (only modified packages are tested/deployed)
  • Scalability (adding new packages should be trivial)

Monorepo tools like Turborepo and Nx have emerged as patterns for modern project scaffolding. They provide structure, convention and automation that transform project setup from repetitive task to efficient, consistent process.

The problem with traditional scaffolding

Fragmented approach

bash# Traditional project setup with multiple packages
# 1. Frontend
$ npx create-react-app frontend

# 2. Backend
$ npx express-generator backend

# 3. Shared
$ mkdir shared && cd shared
$ npm init -y
$ npm install typescript @types/node --save-dev

# 4. Manual config duplication
# Copia ESLint, Prettier, tsconfig between all packages
# 5. Manual dependency management
# npm install in each package, inconsistent versions
# 6. Manual build scripts
# npm run build in each package, without parallelization

Problems:

  • Config drift: ESLint, TypeScript configs diverge between packages
  • Inconsistent dependencies: React 18 in one package, 17 in another
  • Slow builds: No caching, everything is rebuilt every time
  • Manual overhead: Adding new package is complex manual process
  • Inefficient CI: All packages tested even without changes

Impact on development

┌─────────────────────────────────────────────────────────────────────┐
│                     TRADITIONAL SETUP                           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Developer: "I want to add a new shared component"          │
│                                                                 │
│  1. Create folder in shared/                                   │
│  2. Configure tsconfig, Jest, ESLint manually                    │
│  3. Add package.json with build scripts                        │
│  4. Update frontend/package.json with path                       │
│  5. Update backend/package.json with path                        │
│  6. Run npm install in each package                             │
│  7. Run build manually to verify                                │
│                                                                 │
│  Time: 2-3 hours of manual work                          │
│  Error chance: High (forgetting to update some package.json)    │
│                                                                 │
└─────────────────────────────────────────────────────────────────────┘

Turborepo: Simplicity with performance

Core concepts

Turborepo is a monorepo tool focused on performance and simplicity:

┌─────────────────────────────────────────────────────────────────────┐
│                        TURBOREPO                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────┐        ┌──────────────┐                   │
│  │  packages/   │        │    turbo.json │                   │
│  │              │        │              │                   │
│  │  - web/      │───────│  pipeline:    │                   │
│  │  - api/      │       │    web#build  │                   │
│  │  - shared/   │       │    dependsOn: │                   │
│  │  - ui/       │       │      ["shared#build"] │             │
│  └─────────────┘        └──────────────┘                   │
│         │                                                   │
│         │                                                   │
│         │    Turbo executes only changes                  │
│         │    with distributed cache                        │
│         ▼                                                   │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │          .turbo/cache/                               │    │
│  │  (Local cache + optional remote)                    │    │
│  └──────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────────┘

Initial setup

bash# Create monorepo with Turborepo
$ npx create-turbo@latest my-monorepo

# Generated structure
my-monorepo/
├── apps/
│   ├── web/          # Frontend app
│   └── docs/         # Docs app
├── packages/
│   ├── ui/           # Shared components
│   └── config/       # Shared configs
├── turbo.json         # Turborepo config
└── package.json       # Root package.json with workspaces
json// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
json// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "test": "turbo run test",
    "lint": "turbo run lint"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "eslint": "^8.0.0",
    "prettier": "^3.0.0",
    "typescript": "^5.0.0"
  }
}

Usage patterns

bash# Develop all apps and packages
$ npm run dev

# Build only changed packages
$ npm run build

# Build specific package and dependencies
$ turbo run build --filter=web

# Test only packages changed since last commit
$ turbo run test --filter...[HEAD~1]

# Clear local cache
$ turbo prune

CI/CD integration

yaml# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Get pnpm store directory
        id: pnpm-cache
        shell: bash
        run: |
          echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT

      - name: Setup pnpm cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      - name: Install dependencies
        run: pnpm install

      - name: Build affected apps
        run: pnpm turbo run build --filter=...[origin/main]

      - name: Test affected apps
        run: pnpm turbo run test --filter=...[origin/main]

Nx: Complete monorepo ecosystem

Core concepts

Nx offers more advanced features than Turborepo, with enterprise focus:

┌─────────────────────────────────────────────────────────────────────┐
│                           NX                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────┐        ┌──────────────┐                   │
│  │  apps/       │        │    nx.json   │                   │
│  │  - web/      │───────│  generators: │                   │
│  │  - api/      │       │  workspace:  │                   │
│  └─────────────┘       │    lib:      │                   │
│         │              │  @nx/react   │                   │
│         │              └──────────────┘                   │
│  ┌─────────────┐        ┌──────────────┐                   │
│  │  libs/       │        │  affected:   │                   │
│  │  - ui/       │        │    deps:     │                   │
│  │  - utils/    │───────│    apps     │                   │
│  └─────────────┘        └──────────────┘                   │
│         │                                                   │
│         │    Nx calculates dependency graph                │
│         │    and executes in parallel with cache                │
│         ▼                                                   │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │        nx/cache (optional remote)                    │    │
│  └──────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────────┘

Initial setup

bash# Create monorepo with Nx
$ npx create-nx-workspace@latest my-nx-workspace

# Choose: Integrated Monorepo
# Choose: Stack (React, Next.js, NestJS, etc.)
# Choose: CI provider (GitHub Actions, GitLab, etc.)
json// nx.json
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/?(*.spec|*.test).ts",
      "!{projectRoot}/tsconfig.spec.json",
      "!{projectRoot}/jest.config.ts",
      "!{projectRoot}/src/test-setup.{ts,js}"
    ],
    "sharedGlobals": []
  },
  "targetDefaults": {
    "build": {
      "cache": true,
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"]
    },
    "test": {
      "cache": true,
      "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
    },
    "lint": {
      "cache": true,
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"]
    }
  },
  "plugins": [
    "@nx/react",
    "@nx/next",
    "@nx/jest",
    "@nx/eslint"
  ]
}

Generators for automatic scaffolding

Nx provides generators to create apps, libs and components automatically:

bash# Create new Next.js app
$ npx nx g @nx/next:app my-app --directory=apps/my-app

# Create new shared lib
$ npx nx g @nx/react:lib my-lib --directory=libs/my-lib --importPath=@my-monorepo/my-lib

# Create component in lib
$ npx nx g @nx/react:component Button --project=my-lib --export

# Create new NestJS service
$ npx nx g @nx/nest:service users --project=api

Result:

  • Configs (tsconfig, Jest, ESLint) created automatically
  • Dependencies added to correct package.json
  • Imports configured with tsconfig paths
  • Build/test scripts updated

Dependency graph visualizer

bash# Visualize project dependency graph
$ npx nx graph

# Opens Nx visual UI
$ npx nx show-project web --web
┌─────────────────────────────────────────────────────────────────────┐
│                  NX DEPENDENCY GRAPH                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                 │
│              ┌──────────┐                                       │
│              │   web    │                                       │
│              └─────┬────┘                                       │
│                    │                                            │
│          ┌─────────┼─────────┐                                 │
│          ▼         ▼         ▼                                 │
│     ┌────────┐ ┌──────┐ ┌────────┐                            │
│     │  ui    │ │utils │ │config │                            │
│     └────────┘ └──────┘ └────────┘                            │
│         │                                                   │
│         └─────────────┐                                     │
│                       ▼                                     │
│                  ┌──────────┐                                │
│                  │   api    │                                │
│                  └──────────┘                                │
│                                                                 │
│  Web depends on ui, utils, config                            │
│  API depends on utils, config                                │
│  Utils depends on config                                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────────┘

pnpm Workspaces: Dependency management

Workspaces configuration

json// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.0.0"
  }
}
yaml# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
bash# Install dependencies in all packages
$ pnpm install

# Add specific dependency
$ pnpm add axios -w  # Add in root workspace
$ pnpm add axios -F web  # Add in workspace web

# Link workspaces locally (no npm link)
$ pnpm install  # Creates symlinks between workspaces

Workspace scripts

json// apps/web/package.json
{
  "name": "web",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "test": "jest"
  },
  "dependencies": {
    "@my-monorepo/ui": "workspace:*",  // Local workspace
    "@my-monorepo/utils": "workspace:*",
    "next": "^14.0.0",
    "react": "^18.0.0"
  }
}

Advanced monorepo patterns

1. Shared configs (ESLint, Prettier, TypeScript)

typescript// packages/config/eslint/index.ts
import { config } from '@my-monorepo/tsconfig';

export default [
  {
    ignores: ['node_modules', 'dist', '.next'],
    extends: ['next/core-web-vitals', 'prettier'],
    rules: {
      '@typescript-eslint/no-unused-vars': 'error',
      'react-hooks/rules-of-hooks': 'error',
      // Shared rules
    },
  },
  // Custom config per project when needed
  config.eslintOverrides || {},
];
typescript// apps/web/.eslintrc.js
module.exports = require('@my-monorepo/config/eslint');
// Uses shared config, can add project-specific overrides

2. Shared types

typescript// packages/types/src/index.ts
export interface User {
  id: string;
  email: string;
  name: string;
}

export interface Product {
  id: string;
  name: string;
  price: number;
}

export type ApiError = {
  code: string;
  message: string;
  details?: unknown;
};
typescript// apps/web/src/api/users.ts
import { User, ApiError } from '@my-monorepo/types';

async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw await response.json() as ApiError;
  }
  return response.json();
}

3. Shared utilities

typescript// packages/utils/src/date.ts
export function formatDate(date: Date): string {
  return new Intl.DateTimeFormat('en-US', {
    dateStyle: 'medium',
  }).format(date);
}

export function parseDate(dateString: string): Date {
  return new Date(dateString);
}

export function addDays(date: Date, days: number): Date {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}
typescript// packages/utils/src/api.ts
import axios from 'axios';

export const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export async function fetchWithError<T>(
  url: string
): Promise<T> {
  try {
    const response = await api.get<T>(url);
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      throw {
        code: error.code,
        message: error.message,
      } as ApiError;
    }
    throw error;
  }
}

Comparing Turborepo vs Nx

When to choose Turborepo

Turbo is ideal when:

  • You want simplicity and performance
  • Your team already knows standard tools (Next.js, Jest, ESLint)
  • You don't need complex generators
  • You prefer manual configuration over automation

Advantages:

  • Quick, simple setup
  • Excellent performance
  • Low learning curve
  • Natural integration with modern frameworks

When to choose Nx

Nx is ideal when:

  • You have large team with multiple squads
  • You need generators for consistency
  • You want dependency visualization
  • You need enterprise integrations (Jira, Azure DevOps)
  • You want to automate repetitive patterns

Advantages:

  • Powerful generators for scaffolding
  • Visual dependency graph
  • Enterprise tool integrations
  • Greater workflow automation

Migration strategies

Migrating from separate repos to monorepo

bash# 1. Create monorepo
$ npx create-turbo@latest my-monorepo

# 2. Move existing apps
$ mv ../my-app apps/web
$ mv ../my-api apps/api

# 3. Create packages structure
$ mkdir -p packages/ui packages/utils packages/config

# 4. Extract shared code
$ # Move components to packages/ui
$ # Move utils to packages/utils

# 5. Update imports
$ # Swap './components/Button' for '@my-monorepo/ui/Button'

# 6. Configure workspaces
$ # Update package.json with workspaces
$ # Create pnpm-workspace.yaml

# 7. Update CI
$ # Configure turbo for build/test only affected

Migrating from Turborepo to Nx (or vice versa)

The packages/apps structure is compatible between both:

bash# From Turborepo to Nx
$ npx add-nx-to-turbo-workspace
# Converts turbo.json to nx.json
# Adds Nx generators
# Updates scripts

# From Nx to Turborepo
$ npx @nx/workspace-to-turbo
# Converts nx.json to turbo.json
# Removes generators (Turbo doesn't have them)
# Updates scripts

60-day implementation plan

Week 1-2: Selection and setup

  • Choose between Turborepo and Nx
  • Create initial monorepo with scaffolding
  • Configure basic CI/CD

Week 3-4: Gradual migration

  • Migrate one existing app
  • Extract shared code to packages
  • Update imports and dependencies

Week 5-6: Optimization and automation

  • Configure remote cache (Nx Cloud or Turborepo Remote)
  • Create custom generators (if using Nx)
  • Document development patterns
  • Train team

Success metrics

  • Build time: Full build < 2 minutes (with cache)
  • Dev startup: npm run dev starts in < 10 seconds
  • Cache hit rate: > 80% of builds use cache
  • Time to new package: Add new package < 15 minutes
  • CI time: CI/CD for PR < 5 minutes (build + test)

Does your team waste time with repetitive setups and slow builds? Talk to Imperialis specialists about modern project scaffolding, from monorepo to CI/CD optimization, to transform setup into productivity.

Sources

Related reading