Workspaces, Monorepos and Shipping a Package
AdvancedHow a frontend, a backend and a shared library live in one repository — and what changes when you publish one of them.
Overview
Once a product has more than one deployable, the shared code question arrives: duplicate it, publish it, or put everything in one repository. Workspaces make the third option practical — several packages, one install, and local packages linked as if they were published. It is worth understanding even if you never configure one, because a large share of professional codebases are laid out this way and knowing the shape makes them navigable rather than intimidating.
Workspaces
One install, several packages, local links instead of publishing.
// root package.json
{ "private": true, "workspaces": ["apps/*", "packages/*"] }
// apps/web/package.json
{ "dependencies": { "@aicancode/shared": "workspace:*" } }
// Layout
// apps/web Next frontend
// apps/api Node backend
// packages/shared types + zod schemas used by both
// packages/ui component library
npm install // installs everything once
npm run build -w apps/web // run a script in one workspace
// The payoff: change a schema in packages/shared and both apps
// fail to typecheck immediately — no publish step, no version drift.
// The cost: slower CI unless you cache and only build what changed.
// Turborepo or Nx exist for exactly that.Publishing a Package
The fields that decide whether your package works for consumers.
{
"name": "@aicancode/shared",
"version": "1.2.0",
"type": "module",
"files": ["dist"], // what is actually published
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"sideEffects": false // lets consumers tree-shake it
}
// exports replaces "main" and is a hard boundary — consumers can
// import only the paths you list, so internals stay internal.
// Ship the .d.ts files or TypeScript consumers get 'any'.
// tsup or unbuild handle the ESM + CJS + types build in one step.
// Versioning is a promise: patch = fix, minor = additive,
// major = something broke. Changesets automates the changelog.Working on a Dependency Locally
Testing a change to a library against a real app before publishing it.
# In the library
npm link
# In the consuming app
npm link @aicancode/shared
# Common failure: two copies of React in the tree, which produces
# "Invalid hook call". Check with:
npm ls react
# Fix by marking react a peerDependency in the library and
# pointing the bundler at the app's copy.
# Often simpler than linking — pack and install the real tarball,
# which tests what consumers will actually receive:
npm pack # -> aicancode-shared-1.2.0.tgz
npm install ../shared/aicancode-shared-1.2.0.tgzKey Points to Remember
- 1Workspaces install once and link local packages, so a shared schema change breaks both apps at compile time
- 2The exports field defines the package's public surface and replaces main — unlisted paths cannot be imported
- 3Publish the .d.ts files or TypeScript consumers see any; set sideEffects: false so they can tree-shake
- 4Semver is a promise to consumers: patch fixes, minor adds, major breaks
- 5npm link can produce duplicate React copies and "Invalid hook call" — npm pack tests the real published artifact
Interview Questions
Sign in to ask AriaWhat problem do npm workspaces solve in a multi-app repository?
What does the exports field in package.json do?
Why does npm link sometimes cause an "Invalid hook call" error?
Ask Aria about Workspaces, Monorepos and Shipping a Package
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.