Host multiple Nest applications and shared libraries in one repo using the Nest CLI’s built-in monorepo mode. No
nx, noturborepo, no extra config: one command (nest g app) flips a standard project into a workspace, andnest g librarylets you share modules across apps via@app/*path aliases.
When to reach for it
- Two or more Nest services that share auth, DB models, or config.
- A team that wants one
package.json, one lint config, one CI pipeline. - You’re considering
nxorturborepoand want to know what Nest gives you out of the box.
When not to
- Single application, single deploy: stick with standard mode.
- Multi-language repo (Nest API + Go service + Rust worker): use
nx,turborepo, orpnpmworkspaces. Nest CLI only knows about Nest projects. - You need fine-grained task graphs, remote build cache, or affected-only test runs: that’s
nx/turborepoterritory.
Setup
Nothing extra. The Nest CLI ships with the monorepo schematics. Install globally:
npm i -g @nestjs/cli@latestStep 1: scaffold a standard project
nest new my-app
cd my-appThis is a single-project layout: src/, test/, package.json, tsconfig.json, nest-cli.json. Standard mode.
Step 2: convert to a monorepo by adding a second app
nest g app my-app-2The schematic detects you’re in a single-project workspace and rewrites the layout in place:
apps/
my-app/
src/
test/
tsconfig.app.json
my-app-2/
src/
test/
tsconfig.app.json
nest-cli.json
package.json
tsconfig.json
A few things just changed:
- The original
src/andtest/were moved underapps/my-app/. - A new
apps/my-app-2/was created with the standard starter structure. - Each app got its own
tsconfig.app.jsonthat extends the roottsconfig.json. nest-cli.jsonswitched to monorepo mode ("monorepo": true) and the original app was registered as the default project via the top-level"root"property. Source: CLI properties.
Conversion only works on canonical layouts
The schematic relocates
src/andtest/intoapps/<name>/. If you’ve moved or renamed these folders, the conversion fails or produces unreliable results. Re-source from a freshnest newif your project diverged. Source: Monorepo conversion warning.
After conversion, nest-cli.json looks roughly like this:
{
"collection": "@nestjs/schematics",
"sourceRoot": "apps/my-app/src",
"monorepo": true,
"root": "apps/my-app",
"compilerOptions": {
"webpack": true,
"tsConfigPath": "apps/my-app/tsconfig.app.json"
},
"projects": {
"my-app": {
"type": "application",
"root": "apps/my-app",
"entryFile": "main",
"sourceRoot": "apps/my-app/src",
"compilerOptions": { "tsConfigPath": "apps/my-app/tsconfig.app.json" }
},
"my-app-2": {
"type": "application",
"root": "apps/my-app-2",
"entryFile": "main",
"sourceRoot": "apps/my-app-2/src",
"compilerOptions": { "tsConfigPath": "apps/my-app-2/tsconfig.app.json" }
}
}
}Two facts to internalize from this file:
"webpack": trueis the monorepo default (in standard mode the default istsc). Nest assumes monorepos benefit from webpack’s bundling. You can flip it totscorswcvia thebuilderfield. Source: Specified compiler.- The top-level
"root"points at the default project. Everynestcommand without a project name targets it.
Both apps default to port 3000
Each app generated by
nest g appgets the standard startermain.tswithawait app.listen(3000). The schematic does not auto-increment. Run them sequentially and you won’t notice; boot both at once (see step 4) and you getEADDRINUSE: address already in use :::3000. Editapps/my-app-2/src/main.tsnow (or read the port fromprocess.env.PORT) before you forget.
Step 3: targeting a specific app
The default project is implicit:
nest build # builds my-app only
nest start # starts my-app only
nest start --watch # watch mode on my-appTo target the other app, pass its name:
nest build my-app-2
nest start my-app-2
nest start --watch my-app-2dist/ ends up structured per project:
dist/
apps/
my-app/main.js
my-app-2/main.js
package.jsonscripts target the default project onlyThe original
start,start:dev,build,testscripts thatnest newgenerated still exist after conversion, and they still target only the default project (because they don’t pass a name). Add per-app scripts yourself; see step 4.
Step 4: run multiple apps in one terminal
Two separate nest start --watch shells get old fast. The CLI has no built-in “start all” or “build all”. Use concurrently:
npm i -D concurrentlyAdd per-app scripts and a fan-out script that uses concurrently’s npm: shortcut with a wildcard:
{
"scripts": {
"start:dev:my-app": "nest start my-app --watch",
"start:dev:my-app-2": "nest start my-app-2 --watch",
"start:dev": "concurrently -c auto 'npm:start:dev:*'"
}
}Run:
npm run start:devOutput (interleaved with auto-assigned colored prefixes):
[my-app] [Nest] LOG [NestApplication] Nest application successfully started
[my-app-2] [Nest] LOG [NestApplication] Nest application successfully started
[my-app] GET /cats 200 4ms
[my-app-2] GET /orders 200 7ms
How the npm: shortcut works: concurrently 'npm:start:dev:*' expands to every script whose name matches the pattern. npm:start:dev:* matches start:dev:my-app and start:dev:my-app-2, runs both in parallel, and uses whatever the * matched as each process’s prefix. Source: Command Shortcuts.
The
-cflag controls prefix colors
-c(alias of--prefix-colors) decides how each process’s[name]prefix is colored. Three forms:
Form Behavior -c "auto"Picks a distinct color per process automatically. Scales to N processes without editing. -c "cyan.bold,red.bold"Explicit list, one entry per process, applied in spawn order. Chalk names + modifiers ( .bold,.dim) or hex (#RRGGBB).-comittedNo colors, plain [name]prefix in default terminal color.Without
-c, prefixes are not colored by default. The recipe usesautobecause the wildcard can match a varying number of scripts; pin explicit colors only when you want consistency across runs. Source:prefixColorsoption.
Using yarn, pnpm, or bun instead
concurrentlyships first-class shortcuts for all four runners. Swap the prefix in the script and use the matching install/run commands; nothing else in the recipe changes. Source: Command Shortcuts.
Shortcut Expands to Install + run npm:<script>npm run <script>npm i -D concurrently/npm run start:devyarn:<script>yarn run <script>yarn add -D concurrently/yarn start:devpnpm:<script>pnpm run <script>pnpm add -D concurrently/pnpm start:devbun:<script>bun run <script>bun add -d concurrently/bun run start:devAt scaffold time,
nest newaccepts--package-manager npm|yarn|pnpm(nobun); for bun, scaffold withnpmthen re-install withbun install. Source:nest newreference.
Step 5: share code with libraries
Libraries are the monorepo’s reuse primitive: one folder, one TS path alias, every app imports it like an npm package.
nest g library popcornThe CLI prompts:
What prefix would you like to use for the library (default: @app)?
Press enter to accept @app. The schematic creates:
libs/
popcorn/
src/
index.ts
popcorn.module.ts
popcorn.service.ts
tsconfig.lib.json
It also updates the root tsconfig.json with a paths mapping:
{
"compilerOptions": {
"paths": {
"@app/popcorn": ["libs/popcorn/src"],
"@app/popcorn/*": ["libs/popcorn/src/*"]
}
}
}That’s the whole magic: webpack and tsc resolve @app/popcorn to libs/popcorn/src/index.ts. No symlinks, no npm link, no publishing. Source: Using libraries.
The generated module exports a service:
// libs/popcorn/src/popcorn.module.ts
import { Module } from "@nestjs/common"
import { PopcornService } from "./popcorn.service"
@Module({
providers: [PopcornService],
exports: [PopcornService],
})
export class PopcornModule {}// libs/popcorn/src/popcorn.service.ts
import { Injectable, Logger } from "@nestjs/common"
@Injectable()
export class PopcornService {
private readonly logger = new Logger(PopcornService.name)
getPopcorn(): string {
this.logger.log("🍿")
return "🍿"
}
}// libs/popcorn/src/index.ts
export * from "./popcorn.module"
export * from "./popcorn.service"Consume it from any app:
// apps/my-app/src/app.module.ts
import { Module } from "@nestjs/common"
import { PopcornModule } from "@app/popcorn"
import { AppController } from "./app.controller"
import { AppService } from "./app.service"
@Module({
imports: [PopcornModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}// apps/my-app/src/app.service.ts
import { Injectable } from "@nestjs/common"
import { PopcornService } from "@app/popcorn"
@Injectable()
export class AppService {
constructor(private readonly popcorn: PopcornService) {}
getHello(): string {
return this.popcorn.getPopcorn()
}
}Hit the default route:
curl http://localhost:3000Response (and a 🍿 log line in the terminal):
🍿
my-app-2 can import the same library the same way. Both apps now share PopcornService without copy-paste.
Build a library on its own
nest build popcorncompiles the library standalone, useful in CI to gate library changes before the apps that consume it. Source: Creating libraries.
Libraries have no
main.tsA library is
"type": "library"innest-cli.jsonwith"entryFile": "index"(vs"main"for apps). It can’t run on its own — it’s only useful when an app imports it. Source: Libraries metadata.
What gets shared, what stays per-app
| Item | Scope | Where it lives |
|---|---|---|
package.json + lockfile | Workspace-wide | Root |
node_modules/ | Workspace-wide | Root (single install) |
| Lint config, prettier | Workspace-wide | Root |
Root tsconfig.json | Workspace-wide | Root (extended by every project) |
Per-app tsconfig.app.json | Per app | apps/<name>/tsconfig.app.json |
Per-lib tsconfig.lib.json | Per library | libs/<name>/tsconfig.lib.json |
main.ts | Per app | apps/<name>/src/main.ts |
index.ts (entry export) | Per library | libs/<name>/src/index.ts |
Path alias @app/<lib> | Workspace-wide | Root tsconfig.json#compilerOptions.paths |
The single node_modules is the headline tradeoff. Two apps cannot pin different versions of the same package: that’s the price for shared installs and shared CI.
Pros and cons
Pros:
- Zero extra tooling: it’s just the Nest CLI.
- Shared
node_modules, lint, CI, and types. - Libraries are first-class —
@app/<name>works in every editor. - Convert standard → monorepo with one command, no migration script.
Cons:
- No task graph: the CLI doesn’t know which apps depend on which libraries, so a library change always rebuilds everything.
- No remote cache, no affected-only commands. If you need either, reach for
nxorturborepo(you can layer them on top of Nest’s monorepo mode). - Single
package.json: every app gets every dep. You can’t isolate a footgun dep to one app. - No built-in “start all” / “build all”: you wire
concurrentlyyourself.
Gotchas
Renaming the original app needs a vault-wide find-and-replace
The original project keeps its name (e.g.,
my-app) innest-cli.json’s"root", the"projects"map, everytsconfig.app.jsonpath, and any npm scripts you’ve added. If you rename it, search the repo for the old name and update every match. The CLI does not provide a rename command.
Webpack is the default compiler in monorepos
A standard-mode project compiles with
tsc; the same code in monorepo mode compiles with webpack by default. Behaviorally identical for most code, but if you rely ontsc-only features (decorators metadata emit nuances, plugin transformers), set"builder": { "type": "tsc" }innest-cli.json#compilerOptions. Source: Specified compiler.
The library prefix is global per workspace
The first
nest g libraryprompt picks the prefix (@appby default). Subsequent libraries inherit it. Mixing prefixes is possible but means hand-editingtsconfig.jsonpaths — and reviewers reading@platform/authnext to@app/billingwill rightly ask why. Pick one and stick with it.
You can layer
nxorturborepoon topNest’s monorepo mode is just folder layout + a CLI config. Nothing stops you from putting
nx.jsonorturbo.jsonnext tonest-cli.jsonand using their task graphs to orchestratenest build <name>calls. The Nest CLI doesn’t fight you.
Libraries can be built standalone for publishing
If you ever want to publish a library to npm,
nest build <lib>produces a clean dist you can ship. Add a per-librarypackage.jsonand you’re done. The reverse path (npm package → workspace library) is not supported by the CLI; you’d copy the source in manually.
Common errors
| Symptom | Likely cause |
|---|---|
EADDRINUSE: address already in use :::3000 after start:dev | Both apps default to port 3000. Change one, or read from process.env.PORT |
nest build only compiles one app | Working as designed: build targets the default project. Add per-app scripts or a build:all fan-out |
Cannot find module '@app/<lib>' | The lib was created outside the workspace, or root tsconfig.json#paths got hand-edited and broke. Regenerate or restore the path mapping |
nest g library prompts for a prefix every time | That’s the schematic’s behavior. Press enter to keep the default |
| Library changes don’t show up in the app | Restart the dev server: webpack’s incremental rebuild watches the lib, but cold-cached builds need a kick |
concurrently 'npm:start:dev:*' runs nothing | No matching scripts. Run npm run to list scripts and confirm naming. The wildcard is exact-prefix, not glob |
See also
- Recipes hub: other task-oriented guides.
- Fundamentals: the building blocks each app in the monorepo uses.
- Official: Workspaces (monorepo), Libraries.
concurrentlyshortcuts: thenpm:prefix and wildcard matching.- Source video: Marius Espejo, How I manage multiple NestJS applications.