Workspaces and monorepos

npm · Practical · 15 min

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 (-ws for 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/web shows 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=0

Remember

  • Root workspaces globs list member packages
  • One install, one lockfile, symlinked local packages
  • -w <name> targets one workspace, -ws all of them
  • Add Turborepo/Nx for task graphs and caching

Watch out for

  • Forgetting "private": true on the root and publishing the whole monorepo by accident.

Check yourself

  1. How does `apps/web` get the local `@acme/ui` code without publishing it?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.npmjs.com/cli/v11/using-npm/workspaces