Monorepo & Workspaces
As a system grows you rarely ship a single Nest application. You end up with an HTTP API, a background worker, maybe a WebSocket gateway, and a pile of shared code — DTOs, guards, database entities — that all of them depend on. Nest’s built-in monorepo mode turns a single repository into a workspace that hosts many apps and reusable libs behind one node_modules, one tsconfig, and one build pipeline. This page covers enabling monorepo mode, generating shared libraries, wiring path mappings, building selectively, and how all of this compares to running Nest inside an Nx workspace.
Enabling monorepo mode
A fresh nest new project is a standard (single-app) structure. Nest converts it to a monorepo automatically the first time you generate a second application or a library. You can also do it explicitly.
# Start from a standard project
nest new my-platform
cd my-platform
# Generating a second app flips the workspace into monorepo mode
nest generate app worker
After this command the layout changes. Your original src moves under apps/, a sibling app is created, and a nest-cli.json describes the workspace.
my-platform/
├── apps/
│ ├── my-platform/ # the default app
│ │ ├── src/
│ │ └── tsconfig.app.json
│ └── worker/
│ ├── src/
│ └── tsconfig.app.json
├── libs/ # shared libraries live here
├── nest-cli.json
├── package.json # ONE package.json for the whole workspace
└── tsconfig.json
The key idea: every app and lib shares one dependency tree and one root tsconfig.json. There is no per-package node_modules, which keeps versions consistent and installs fast.
Generating a shared library
Libraries are where reusable code lives. Generate one with the CLI:
nest generate library common
You will be prompted for an import prefix; accept the default @app. Nest scaffolds the lib and registers a TypeScript path mapping so any app can import it by name.
// libs/common/src/logging/app-logger.service.ts
import { Injectable, LoggerService } from '@nestjs/common';
@Injectable()
export class AppLogger implements LoggerService {
log(message: string) { this.write('LOG', message); }
error(message: string) { this.write('ERROR', message); }
warn(message: string) { this.write('WARN', message); }
private write(level: string, message: string) {
process.stdout.write(`${new Date().toISOString()} [${level}] ${message}\n`);
}
}
Expose it through the library’s module and barrel file:
// libs/common/src/common.module.ts
import { Module } from '@nestjs/common';
import { AppLogger } from './logging/app-logger.service';
@Module({
providers: [AppLogger],
exports: [AppLogger],
})
export class CommonModule {}
// libs/common/src/index.ts
export * from './common.module';
export * from './logging/app-logger.service';
Now consume it from any app with the configured alias — no relative ../../../ paths:
// apps/worker/src/app.module.ts
import { Module } from '@nestjs/common';
import { CommonModule } from '@app/common';
@Module({
imports: [CommonModule],
})
export class AppModule {}
Path mappings
The nest generate library command edits the root tsconfig.json for you, adding a paths entry that maps the import prefix to the library source. This is what makes @app/common resolve correctly during both compilation and IDE navigation.
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@app/common": ["libs/common/src"],
"@app/common/*": ["libs/common/src/*"]
}
}
}
The matching libs block appears in nest-cli.json, telling the CLI where each library’s root and tsconfig live:
{
"monorepo": true,
"root": "apps/my-platform",
"sourceRoot": "apps/my-platform/src",
"projects": {
"my-platform": { "type": "application", "root": "apps/my-platform" },
"worker": { "type": "application", "root": "apps/worker" },
"common": { "type": "library", "root": "libs/common", "sourceRoot": "libs/common/src" }
}
}
Always import shared code through the
@app/*alias, never with deep relative paths. The alias is the contract; relative imports break when files move and confusenest build’s dependency detection.
Building selectively
The CLI builds one project at a time. Pass the project name to nest build and nest start; omit it to act on the default project from nest-cli.json.
# Build a single app — Nest compiles only the libs it actually imports
nest build worker
# Run one app in watch mode during development
nest start worker --watch
# Build everything for a release
nest build my-platform && nest build worker
Output:
> nest build worker
✔ Compiling apps/worker + libs/common
dist/apps/worker/main.js
Each app produces an independent bundle under dist/apps/<name>, so deployment artifacts stay isolated. For production you typically build with --webpack to tree-shake unused library code into a single lean main.js per app.
| Task | Standard project | Monorepo mode |
|---|---|---|
| Build target | implicit single app | nest build <project> |
| Run target | nest start | nest start <project> |
| Shared code | npm package or copy | libs/* with path alias |
node_modules | one | one (shared) |
| Output | dist/main.js | dist/apps/<name>/main.js |
Comparison with Nx
Nest’s native monorepo is intentionally lightweight: shared node_modules, path aliases, per-project builds. Nx is a heavier build system you can layer on top for large organizations.
| Capability | Nest monorepo | Nx workspace |
|---|---|---|
| Setup | built into the CLI | npx create-nx-workspace + @nx/nest |
| Affected/incremental builds | no | yes (nx affected) |
| Build caching | no | local + remote cache |
| Dependency graph | basic | full nx graph visualization |
| Polyglot (React, Go, etc.) | Nest-only | many plugins |
| Learning curve | minimal | steeper |
For two or three Nest apps with shared libs, the built-in mode is plenty. Reach for Nx once you need computation caching, distributed task execution, or a graph spanning frontends and backends.
# Scaffold a Nest app inside an Nx workspace instead
npx create-nx-workspace@latest acme --preset=nest
Best Practices
- Keep one library per bounded concern (
@app/auth,@app/database) rather than a single catch-allcommonthat everything imports. - Export only what apps need through the library’s
index.tsbarrel; treat everything else as internal. - Always import via the
@app/*alias so the CLI tracks dependencies correctly and refactors stay safe. - Build each app with
--webpackfor production to tree-shake unused library code. - Use
nest build <project>in CI to build only the apps a change touched, keeping pipelines fast. - Pin all dependencies in the single root
package.jsonso every app and lib runs the same versions. - Adopt Nx only when you genuinely need caching, affected-graph builds, or a polyglot workspace — not before.