Workspaces & Monorepos
npm workspaces link internal packages in one repo so shared code ships without publishing to a registry for every change.
Search across all documentation pages
npm workspaces link internal packages in one repo so shared code ships without publishing to a registry for every change.
Quick-reference recipe card - copy-paste ready.
{
"name": "@acme/platform",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "npm run build -w @acme/shared && npm run build -w @acme/api"
}
}// apps/api/package.json
{
"name": "@acme/api",
"dependencies": {
"@acme/shared": "workspace:*"
}
}When to reach for this:
npm ci for the whole org slice.platform/
package.json # workspaces root
package-lock.json
apps/
api/
package.json # @acme/api
src/server.ts
packages/
shared/
package.json # @acme/shared
src/index.ts
// packages/shared/package.json
{
"name": "@acme/shared",
"version": "0.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json"
}
}// apps/api/src/server.ts
import { createLogger } from "@acme/shared";
const log = createLogger("api");
log.info("listening");npm install # links workspaces
npm run build -w @acme/shared
npm run dev -w @acme/apiWhat this demonstrates:
workspace:* tells npm to symlink the local @acme/shared package.-w targets a single workspace without cd.workspaces globs discover package.json files under apps/* and packages/*.npm install hoists shared dependencies and symlinks internal packages into consumer node_modules.workspace:* resolves to the local version at install time; publishing replaces it with the concrete semver.package-lock.json at the root captures the entire graph.| Specifier | Meaning |
|---|---|
workspace:* | Any local version (most common) |
workspace:^ | Match local major-compatible |
workspace:1.2.3 | Pin to exact local version |
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"
}
}composite for project references across packages.@acme/shared), not relative paths into packages/shared/src.package.json main/exports.dist/. Fix: order build scripts or use Turborepo pipeline deps.import-x/no-extraneous-dependencies.npm publish. Fix: run npm version in the package before external release.@acme/a depends on @acme/b and vice versa. Fix: extract shared kernel to @acme/core.| Alternative | Use When | Don't Use When |
|---|---|---|
| npm pack + file: | Quick spike without workspaces | Long-term monorepo maintenance |
| Private Verdaccio / npm org | Teams need versioned internal releases | Every change is atomic in one PR |
| Turborepo / Nx on top | Cached builds across 5+ packages | Two-package repo (overkill) |
npm replaces workspace:* with the concrete version from the workspace package's package.json when you npm publish from that package.
npm run test -w @acme/api
npm run build --workspaces --if-present-w targets one package; --workspaces runs across all.
Yes. Common layout: apps/* for deployables, packages/* for shared libs. Keep deployables clearly separated in Docker/CI.
No. One root lockfile is the npm workspaces default and preferred for reproducible CI.
npm install zod -w @acme/apiThe root lockfile updates; the dependency lands in that workspace's package.json.
Mark "private": true on packages never published externally. Remove only when publishing to npm with provenance.
Built .d.ts in dist/ plus types field in package.json. Build shared packages before typechecking dependents.
Yes. Each app workspace owns its framework dependency; shared code stays framework-agnostic.
npm may install multiple versions nested in the lockfile. Prefer aligning versions to reduce bundle size and audit surface.
When another repo consumes them or you need semver boundaries between teams. Until then, workspace:* is enough.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 18, 2026