Create the workspace and packages
Use one repository for related packages, but give each publishable unit its own manifest and boundary.
Start with the smallest structure that matches ownership
A single library needs one package.json. This quiz system has an SDK, shared runtime contracts, and a private API used by integration tests. A pnpm workspace lets those packages develop together without pretending they are one publishable unit.
Create the repository
mkdir quiz-sdk
cd quiz-sdk
pnpm init
pnpm add -D typescript vitest
pnpm init creates the root manifest. TypeScript and Vitest belong in root devDependencies because maintainers need them to build and test. SDK consumers do not.
Inspect generated files
Initialization tools choose defaults. They do not know your supported runtimes or release policy. Read every generated field. If a tool rejects a package-manager range, use an exact version such as 11.20.0.
Protect the workspace root
{
"name": "quiz-sdk-workspace",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^7.0.0",
"vitest": "^4.0.0"
}
}
private: true prevents accidental publication of the repository root. The root coordinates work. It is not a package for consumers.
Tell pnpm where packages live
Create pnpm-workspace.yaml:
packages:
- "packages/*"
This pattern makes every direct child of packages a workspace project. pnpm can then link local dependencies and run commands across the repository.
Give every package an identity
Start public packages with private: true as a temporary safety lock. Remove that field only after packing and consumer tests pass.
{
"name": "@your-scope/quiz-sdk",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": "./src/index.ts"
}
Use a real npm username or organization in place of your-scope. A scope prevents global name collisions and groups related packages.
Keep the API private permanently:
{
"name": "@your-scope/quiz-api",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": "./src/index.ts"
}
Link packages with workspace:
{
"dependencies": {
"@your-scope/quiz-contracts": "workspace:*"
}
}
During development, workspace:* requires pnpm to use the local contracts package. During packing, pnpm replaces it with the contracts package version, such as 0.1.0. Consumers never see the workspace protocol.
Why each workspace package needs a version
pnpm cannot replace workspace:* if the target package has no version. Even a private API used as a development dependency needs a version when it appears in a package manifest that pnpm packs.
Create the lockfile and links
pnpm install
pnpm typecheck
pnpm test
pnpm install resolves dependencies, writes pnpm-lock.yaml, and creates workspace links. Commit the lockfile so local development and CI resolve the same dependency graph.
Official references
Check your understanding
Why is the root package private?
Show answer
The root coordinates packages and development tools. Consumers should never install it, so
private: trueblocks accidental publication.What does
workspace:*become when pnpm packs the SDK?Show answer
It becomes the linked package's concrete version, such as
0.1.0. That is a normal registry dependency that any package manager can install.