Monorepos are the norm for apps with shared libraries, and workspaces are npm's built-in support.
Workspaces let one root package.json manage several packages in subfolders:
{ "name": "acme", "private": true, "workspaces": ["packages/*", "apps/*"] }Running npm install at the root installs all workspaces' dependencies into one root node_modules, writes one lockfile, and symlinks each workspace there, so apps/web can import "@acme/ui" and get the local source without publishing.
Working with them:
- Target one workspace with
-w:npm install zod -w apps/api,npm run build -w @acme/ui. - Run in all:
npm run test --workspaces --if-present(-wsfor short). - Add a dependency between workspaces by name and version:
npm install @acme/ui -w apps/web; npm links the local copy when the version matches. npm ls -w apps/webshows one workspace's tree.
Limitations: npm runs workspace scripts in order but has no task graph, caching or "only what changed" logic. Larger monorepos layer Turborepo or Nx on top, or use pnpm workspaces, whose strict, linked node_modules prevents packages from importing dependencies they did not declare.
Mark the root "private": true so it can never be published by accident.
Example
# Layout: package.json (root) packages/ui/package.json apps/web/package.json
cat package.json
# { "name": "acme", "private": true,
# "workspaces": ["packages/*", "apps/*"],
# "scripts": { "test": "npm run test --workspaces --if-present" } }
npm install # one install, one package-lock.json
ls -l node_modules/@acme/ # ui -> ../../packages/ui (symlink)
npm install @acme/ui -w apps/web # local package as a dependency
npm install -D vitest -w packages/ui # dev dependency of one workspace
npm run build -w @acme/ui # run one workspace's script
npm run test -ws --if-present # run everywhere it exists
npm ls -w apps/web --depth=0Remember
- Root
workspacesglobs list member packages - One install, one lockfile, symlinked local packages
-w <name>targets one workspace,-wsall of them- Add Turborepo/Nx for task graphs and caching
Watch out for
- Forgetting
"private": trueon the root and publishing the whole monorepo by accident.