Skip to content
ExponaBLOG
NESTJS · DAY 069 / 100

Nest CLI monorepo mode: a practical guide for developers

Multiple apps, one repository, one toolchain.

Expona Admin8 min read

Introduction

As your NestJS application grows, managing multiple services, shared utilities, and domain-specific libraries in separate repositories becomes a maintenance nightmare. Nest CLI monorepo mode: a practical guide for developers shows you how to consolidate multiple apps and libraries into a single repository while keeping them independently buildable and deployable. We'll walk through setting up monorepo structure, configuring path aliases, and orchestrating builds—the way production teams do it across fintech, e-commerce, and SaaS platforms.

Understanding monorepo architecture with NestJS

A monorepo is a single Git repository containing multiple applications and shared libraries. Instead of managing auth, common DTOs, and logging across three separate repos with version coordination headaches, everything lives in one place with unified tooling and CI/CD.

NestJS provides built-in monorepo support via the CLI. When initialized in monorepo mode, your project gains an apps directory for applications and a libs directory for shared code. Both are compiled independently and can depend on each other through path aliases.

The key advantage: you commit once, test once, deploy multiple apps—each with its own entry point, configuration, and deployment artifact.

Initializing and generating apps with nest g app

Start by creating a monorepo workspace. Instead of building a simple application, scaffold with the monorepo flag:

typescript
npm i -g @nestjs/cli
nest new my-platform --skip-git
cd my-platform
nest g app api-auth
nest g app api-billing

After running nest g app, your structure looks like:

typescript
my-platform/
├── apps/
│   ├── api-auth/
│   │   ├── src/
│   │   ├── test/
│   │   ├── tsconfig.app.json
│   │   └── project.json
│   ├── api-billing/
│   │   ├── src/
│   │   ├── test/
│   │   ├── tsconfig.app.json
│   │   └── project.json
├── libs/
├── nest-cli.json
├── tsconfig.json
└── package.json

Each app is fully scaffolded with its own controller, service, module, and test suite. The project.json file (or angular.json` if using Nx under the hood) defines build, serve, and test configurations per app.

Real-world example: a ride-hailing platform needs an auth service (user login, JWT refresh), a billing service (payment processing, invoices), and a trip service. Rather than three Git repos and coordinated CI pipelines, you generate all three apps in one monorepo and share domain logic via libs.

Creating shared libraries with nest g lib

Shared code lives in the libs directory. Generate a library for common interfaces, database models, or utility functions:

typescript
nest g lib common
nest g lib database
nest g lib auth-guards

Each library gets its own tsconfig.lib.json, a public API entry point (index.ts), and a dedicated directory:

typescript
libs/
├── common/
│   ├── src/
│   │   ├── dto/
│   │   │   ├── pagination.dto.ts
│   │   │   └── error-response.dto.ts
│   │   ├── interfaces/
│   │   └── index.ts (barrel export)
│   ├── tsconfig.lib.json
│   └── package.json
├── database/
│   ├── src/
│   │   ├── entities/
│   │   ├── migrations/
│   │   └── index.ts
│   └── tsconfig.lib.json
├── auth-guards/
│   ├── src/
│   │   ├── jwt.guard.ts
│   │   ├── role.guard.ts
│   │   └── index.ts
│   └── tsconfig.lib.json

Libraries are designed for reuse. Every app can depend on libs/common to access shared DTOs without duplication. The index.ts in each library is a barrel file that exports the public API:

typescript
// libs/common/src/index.ts
export * from './dto/pagination.dto';
export * from './dto/error-response.dto';
export * from './interfaces/user.interface';

In a CRM system, the common library might hold user entity definitions, pagination helpers, and error codes used by both the customer app and the admin app. When you update the pagination DTO, both apps pick it up automatically at build time.

Path aliases: @app and @lib for clean imports

Without path aliases, importing from a sibling library requires relative paths:
import { PaginationDto } from '../../../libs/common/src/dto';

NestJS monorepo mode automatically configures path aliases in tsconfig.json:

typescript
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@app/api-auth": ["apps/api-auth/src"],
      "@app/api-billing": ["apps/api-billing/src"],
      "@lib/common": ["libs/common/src"],
      "@lib/database": ["libs/database/src"],
      "@lib/auth-guards": ["libs/auth-guards/src"]
    }
  }
}

Now any file in any app or library can import cleanly:

typescript
import { PaginationDto, ErrorResponse } from '@lib/common';
import { JwtGuard } from '@lib/auth-guards';
import { User } from '@lib/database';

This pattern scales beautifully. In a travel platform with 10+ apps (booking, payments, notifications, analytics), you can quickly reference shared code without mental overhead.

The alias also appears in your IDE's autocomplete and in compiled JavaScript, making debugging straightforward.

Unified node_modules and configuration

One of the biggest wins of monorepo mode: single node_modules directory and shared configuration files.

All apps and libraries depend on the same versions of NestJS, TypeScript, and third-party packages. If api-auth needs Express 4.18 and api-billing needs Express 4.17, you reconcile that once in the root package.json—no version conflicts across deployments.

Lint, format, and test configuration is inherited from the root:

typescript
// Root .eslintrc.js applies to all apps/libs
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'plugin:@typescript-eslint/recommended',
    'prettier'
  ],
  rules: {
    '@typescript-eslint/explicit-function-return-types': 'error'
  }
};

Each app or lib can extend or override specific rules via its own .eslintrc.js, but the foundation is consistent. This prevents one service from having lax naming conventions while another enforces strict typing.

In a financial trading platform, consistent error handling and nullable checks across the entire codebase—enforced by a shared lint config—prevents subtle bugs that could cost real money.

Building and deploying apps independently

Despite sharing code, each app builds and deploys as its own artifact. Run:

typescript
nest build api-auth
nest build api-billing

Each produces a separate dist folder or, in a monorepo, output directories like:

typescript
dist/
├── apps/
│   ├── api-auth/  (ready to docker build and push)
│   └── api-billing/
└── libs/
    ├── common/
    └── database/

Libraries are compiled as CommonJS or ESM modules and bundled into each app's output. The build system (NestJS CLI or Nx under the hood) traces dependencies and includes only what each app needs.

Deployment happens independently. Your CI/CD pipeline can:

  1. Run tests for only the apps and libs affected by the commit.
  2. Build only api-auth if api-billing's code didn't change.
  3. Deploy api-auth to staging without touching api-billing.

For example, in a SaaS product, a bug fix in the auth library triggers a rebuild and redeploy of the auth service, but the billing service continues running the previous version until you're ready. This reduces blast radius and deployment time.

Docker configuration stays simple:

typescript
# Dockerfile.api-auth
FROM node:18-alpine
WORKDIR /app
COPY dist/apps/api-auth .
COPY package*.json ./
RUN npm ci --omit=dev
CMD ["node", "main.js"]

Practical workflow: building and testing

Here's how a real team works with this structure day-to-day:

Development: Run one app locally while others are mocked or stubbed:

typescript
nest start api-auth --debug --watch

Testing changes across apps: Run tests only for affected code:

typescript
nest test --project api-auth
nest test --project common

Pre-commit: Lint everything to catch issues early:

typescript
eslint apps/ libs/ --fix

CI pipeline (GitHub Actions example):

typescript
- run: npm ci
- run: npm run lint
- run: npm run test
- run: nest build api-auth && docker build -f Dockerfile.api-auth -t api-auth:$GITHUB_SHA .
- run: docker push api-auth:$GITHUB_SHA

Common mistakes

Circular dependencies between libraries: If @lib/auth-guards imports from @lib/common, and @lib/common imports from @lib/auth-guards, the build fails silently or with cryptic errors. Use dependency diagrams in your IDE or run nest build with verbose output to catch these early. Establish a clear dependency hierarchy (e.g., common is foundational, auth-guards depend on common, domain-specific libs build on top).

Exposing internal implementation details: Libraries should export only through their barrel index.ts. If you import @lib/database/src/internals/cache-layer.ts directly, you're breaking encapsulation and coupling other code to internal APIs. Enforce this in code review and ESLint rules (no-restricted-imports).

Forgetting to update tsconfig paths when adding libs: New libraries need entries in the root tsconfig.json` under paths. Miss this and you'll see "module not found" errors despite the library existing. Make lib generation a pre-flight checklist item.

Over-sharing code: Not everything belongs in a shared lib. If only one app uses a service, keep it in that app. Shared libs should be genuinely multi-tenant—used by at least two apps or stable enough to warrant shared maintenance.

Key takeaways

  • Monorepo mode consolidates multiple NestJS applications and shared libraries into one repository with unified tooling and CI/CD.
  • Use nest g app and nest g lib to scaffold apps and libraries; each gets its own build, test, and deployment configuration.
  • Path aliases (@app/*, @lib/*) eliminate relative imports and scale cleanly as the repo grows.
  • A single node_modules, shared lint and test config, and unified version management reduce friction and prevent subtle cross-service bugs.
  • Each app builds and deploys independently, allowing selective testing, targeted deployments, and reduced blast radius during rollouts.
  • Establish clear library boundaries, prevent circular dependencies, and avoid over-sharing code to keep the monorepo maintainable long-term.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles