Node.js 8 🟢 Understanding package.json, package-lock.json, and workspaces
Every Node.js project has a package.json file. It is the project’s manifest, describing what the project is, what it depends on, and how to run its scripts. Alongside it sits package-lock.json, a file that records the exact versions of every dependency in the tree so that installs are reproducible. When a repository contains multiple related packages, the workspaces field in package.json turns the repository into a monorepo, automating the linking of local packages that would otherwise require manual npm link commands.
These three pieces form the foundation of dependency management in Node.js. package.json declares intent: “this project needs Express version 4 or later.” package-lock.json records reality: “the last time this project was installed, Express resolved to 4.18.2 with this specific integrity hash.” The distinction matters because the lockfile is what makes npm ci deterministic, and the lockfile is what prevents one developer from getting a different dependency tree than another. Workspaces extend this model to multiple packages within a single repository, with a shared root lockfile and automatic symlinking.
This chapter covers the structure and fields of package.json, the purpose and mechanics of package-lock.json, the difference between the two lockfile formats, the workspaces field and how npm, pnpm, and yarn implement it, and the patterns for organizing a monorepo.
Key point: package.json declares dependencies with version ranges. package-lock.json records the exact resolved versions and integrity hashes, making installs reproducible. The workspaces field defines local packages that are automatically symlinked during install. Commit package-lock.json for applications; the root lockfile covers all workspaces in a monorepo.
Why these three files exist
The manifest problem. A project needs to describe itself. What is it called? What version is it? What does it depend on? How do you run its tests? Without a standard file, every tool would need its own configuration format. package.json is the single manifest that npm, yarn, pnpm, and the Node.js ecosystem all read.
The reproducibility problem. A dependency declared as "express": "^4.18.0" means “any 4.x version at least 4.18.0.” When a new 4.x version is released, a fresh install picks it up. If the new version has a regression, the build breaks, and the developer who ran npm install yesterday has a different tree than the one who runs it today. The lockfile solves this by recording the exact version that was resolved. npm ci installs exactly what the lockfile specifies, so every environment gets the same tree .
The monorepo problem. A repository with several related packages—a shared UI library, a server, a CLI—needs to reference the packages by name without publishing them to the registry. Manually running npm link in each package is tedious and error-prone. The workspaces field automates this: npm install at the root links the local packages into node_modules so they can be imported by name .
The audit and security problem. The lockfile records the integrity hash of every package. If a package is tampered with on the registry, the hash mismatches, and the install fails. This provides a supply-chain guarantee that package.json alone cannot.
a. The package.json file
package.json is created with npm init (interactive) or npm init -y (defaults) . It must contain name and version fields; everything else is optional .
{
"name": "my-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node src/index.js",
"test": "jest",
"build": "tsc",
"lint": "eslint"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {}
}
The name field must be lowercase and can contain hyphens, dots, and underscores. The version field follows semantic versioning: MAJOR.MINOR.PATCH .
Dependencies are split into two categories. dependencies are packages the application needs at runtime. devDependencies are packages needed only during development: test frameworks, linters, TypeScript, and build tools . When a package is installed with --save-dev, it is added to devDependencies.
Scripts define commands that can be run with npm run <name>. The start and test scripts are special: they can be run with npm start and npm test, omitting the word run . Common scripts include start, build, test, and lint.
Other fields include main, which specifies the entry point for a package; type, which determines whether .js files are treated as ESM or CommonJS; and packageManager, which pins the package manager version for Corepack .
b. The package-lock.json file
package-lock.json is generated automatically whenever npm modifies node_modules or package.json. It describes the exact tree that was generated, so subsequent installs produce identical trees regardless of intermediate dependency updates .
{
"name": "my-project",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-...",
"dependencies": {
"accepts": "~1.3.8"
}
}
}
}
The version field for each package is the exact resolved version. The resolved field is the URL from which the package was downloaded. The integrity field is a hash used to verify the package’s contents .
The lockfile must be committed to version control. It ensures that teammates, deployments, and CI install exactly the same dependencies . It also allows “time travel” to previous states of node_modules without committing the directory itself .
npm ci installs from the lockfile. It fails if package.json and the lockfile are out of sync, which prevents accidental dependency drift. This is the recommended command for CI environments.
npm install updates the lockfile if package.json has changed. It reconciles the declared ranges with the resolved versions and records the result.
The lockfile version depends on the npm version. Lockfile version 2 is used by npm 7 and 8. Version 3 is used by npm 9 and later. npm always attempts to read older lockfile versions and update them .
c. The package-lock.json vs npm-shrinkwrap.json
Both files have the same format and serve the same purpose in the root of a project. The difference is publication .
package-lock.json cannot be published and is ignored if found outside the root project. npm-shrinkwrap.json can be published and defines the dependency tree from the point it is encountered. This is useful for CLI tools that need to ship with a locked dependency tree, but it is not recommended for general use .
If both files are present, npm-shrinkwrap.json takes precedence, and package-lock.json is ignored .
d. The workspaces field
Workspaces allow a single repository to contain multiple packages that can reference each other without publishing. The root package.json declares the workspaces:
{
"name": "my-monorepo",
"version": "1.0.0",
"workspaces": [
"packages/*",
"apps/*"
]
}
Running npm install at the root symlinks each workspace into node_modules . A workspace named packages/ui becomes importable as require('ui') or import from 'ui' from any other workspace .
Adding dependencies to a workspace uses the -w flag:
npm install express -w packages/server
This adds Express to the workspace’s package.json and installs it at the root node_modules .
Adding a workspace as a dependency of another workspace uses the same command. npm detects that the target is a local workspace and symlinks it instead of fetching from the registry .
Running scripts in workspaces:
npm run test --workspace=packages/ui
npm run test --workspaces
The first runs the script in one workspace; the second runs it in all workspaces .
e. Workspaces in pnpm and yarn
pnpm and yarn implement workspaces differently, with distinct configuration files and protocols.
pnpm uses a pnpm-workspace.yaml file at the root instead of the workspaces field in package.json :
packages:
- 'packages/*'
- 'apps/*'
pnpm introduces the workspace: protocol for dependencies. "foo": "workspace:*" means “link to the local foo package, whatever its version.” "foo": "workspace:^1.0.0" means “link only if the local version satisfies ^1.0.0.” Before publishing, the protocol is replaced with the actual version . This prevents accidentally installing a registry version when a local version was intended.
yarn uses the same workspaces field as npm in package.json. Yarn Modern (v2+) supports the workspace: protocol, but Yarn Classic (v1) does not understand it, which causes failures when a repository uses the protocol and a tool defaults to Yarn Classic . The fix is to set the packageManager field in package.json so Corepack uses the correct version:
{
"packageManager": "yarn@4.5.3"
}
Complete Example Session
# ============================================
# PART 1: CREATE A PACKAGE.JSON
# ============================================
mkdir my-project && cd my-project
npm init -y
// ============================================
// PART 2: EXAMINE THE GENERATED FILE
// ============================================
{
"name": "my-project",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
# ============================================
# PART 3: INSTALL DEPENDENCIES
# ============================================
npm install express
npm install --save-dev typescript
// ============================================
// PART 4: EXAMINE UPDATED PACKAGE.JSON
// ============================================
{
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
# ============================================
# PART 5: EXAMINE PACKAGE-LOCK.JSON
# ============================================
cat package-lock.json
# Contains exact resolved versions and integrity hashes
# ============================================
# PART 6: REPRODUCIBLE INSTALL
# ============================================
rm -rf node_modules
npm ci
# Installs exactly what is in the lockfile
# ============================================
# PART 7: SET UP A WORKSPACE
# ============================================
mkdir -p packages/ui packages/server
npm init -w packages/ui -y
npm init -w packages/server -y
// ============================================
// PART 8: EXAMINE ROOT PACKAGE.JSON
// ============================================
{
"name": "my-project",
"workspaces": [
"packages/ui",
"packages/server"
]
}
# ============================================
# PART 9: ADD DEPENDENCY TO A WORKSPACE
# ============================================
npm install lodash -w packages/ui
# ============================================
# PART 10: LINK WORKSPACES
# ============================================
npm install lodash -w packages/server
# Or reference the ui package from server:
npm install ui -w packages/server
# npm symlinks the local workspace
These ten parts cover initialization, installing dependencies, examining both files, reproducible installs, setting up workspaces, and linking local packages.
Quick Reference
package.json Fields
| Field | Purpose |
|---|---|
name | Package name (required) |
version | Semver version (required) |
description | Package description |
main | Entry point |
type | module for ESM, commonjs for CJS |
scripts | Runnable commands |
dependencies | Runtime dependencies |
devDependencies | Development dependencies |
workspaces | Local package globs |
packageManager | Pins package manager version |
package-lock.json Facts
| Fact | Detail |
|---|---|
| Generated by | npm install |
| Committed | Yes, for applications |
| Used by | npm ci for reproducible installs |
| Contains | Exact versions, URLs, integrity hashes |
| lockfileVersion 3 | npm 9+ |
Workspaces Commands
| Command | Purpose |
|---|---|
npm install at root | Links all workspaces |
npm install pkg -w <workspace> | Add dependency to workspace |
npm run test --workspace=a | Run script in one workspace |
npm run test --workspaces | Run script in all workspaces |
Workspace Protocol (pnpm)
| Specifier | Meaning |
|---|---|
workspace:* | Link any local version |
workspace:^ | Link if local satisfies ^ |
workspace:~ | Link if local satisfies ~ |
workspace:1.5.0 | Link exact version |
Best Practices
✅ Do This:
npm init -y # Quick init
npm install express # Add to dependencies
npm install --save-dev typescript # Add to devDependencies
npm ci # CI reproducible install
npm install lodash -w packages/ui # Add to workspace
git add package-lock.json # Commit lockfile
❌ Don’t Do This:
rm package-lock.json # ❌ Destroys reproducibility
npm install -g express # ❌ Global install for project dep
npm install --save express # ❌ --save is default
# No packageManager field with Yarn PnP # ❌ Renovate fails
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
npm ci fails | Lockfile out of sync with package.json | Run npm install locally and commit lockfile |
| Different versions on machines | Lockfile not committed | Always commit package-lock.json |
| Workspace not resolving | Not run npm install at root | Run npm install to symlink |
workspace: protocol fails | Yarn Classic used instead of Modern | Set packageManager field |
| Phantom dependency | Transitive dep hoisted to top | Use pnpm or check imports |
| Lockfile conflicts in git | Multiple developers installing different versions | Resolve by regenerating with npm install |
Real-World Examples
1. Create package.json
npm init -y
2. Add Runtime Dependency
npm install express
3. Add Dev Dependency
npm install --save-dev jest
4. Reproducible Install
npm ci
5. Define Workspaces
{
"workspaces": ["packages/*", "apps/*"]
}
6. Add Dependency to Workspace
npm install axios -w packages/api-client
7. Run Script in Workspace
npm run build --workspace=packages/ui
8. Run Script in All Workspaces
npm run test --workspaces
9. Pin Package Manager
{
"packageManager": "pnpm@9.0.0"
}
10. pnpm Workspace Protocol
{
"dependencies": {
"ui": "workspace:*"
}
}
Visual
package.json vs package-lock.json
┌──────────────────────────────────────────────────────────────┐
│ package.json package-lock.json │
│ ───────────── ───────────────── │
│ Declares intent Records reality │
│ Version ranges Exact versions │
│ "express": "^4.18.0" "express": "4.18.2" │
│ Edited by developer Generated by npm │
│ Committed Committed │
│ │
│ npm install npm ci │
│ └── Resolves ranges └── Reads lockfile │
│ Updates lockfile Fails if out of sync │
└──────────────────────────────────────────────────────────────┘
Workspace Linking
┌──────────────────────────────────────────────────────────────┐
│ monorepo/ │
│ ├── package.json {"workspaces": ["packages/*"]} │
│ ├── package-lock.json (covers all workspaces) │
│ └── packages/ │
│ ├── ui/ │
│ │ └── package.json {"name": "ui"} │
│ └── server/ │
│ └── package.json {"name": "server"} │
│ │
│ After npm install at root: │
│ node_modules/ │
│ ├── ui -> ../packages/ui (symlink) │
│ └── server -> ../packages/server (symlink) │
│ │
│ server can now: require('ui') │
└──────────────────────────────────────────────────────────────┘
Lockfile Versions
┌──────────────────────────────────────────────────────────────┐
│ npm version lockfileVersion Notes │
│ ───────────────┼─────────────────┼──────────────────────── │
│ npm 5–6 │ 1 │ Legacy format │
│ npm 7–8 │ 2 │ Backwards compatible │
│ npm 9+ │ 3 │ Current, hidden lockfile │
└──────────────────────────────────────────────────────────────┘
Workspace Protocol (pnpm)
┌──────────────────────────────────────────────────────────────┐
│ "ui": "workspace:*" │
│ └── Links to local ui package, any version │
│ │
│ "ui": "workspace:^1.5.0" │
│ └── Links only if local version satisfies ^1.5.0 │
│ │
│ Before publish: │
│ "ui": "workspace:*" → "ui": "1.5.0" │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
package.json | Project manifest with name, version, dependencies, scripts |
package-lock.json | Exact resolved dependency tree, integrity hashes |
| Lockfile purpose | Reproducible installs across environments |
npm ci | Installs from lockfile, fails if out of sync |
workspaces | Local packages linked automatically |
| Workspace command | npm install pkg -w <workspace> |
| pnpm workspace config | pnpm-workspace.yaml |
| pnpm protocol | workspace:*, workspace:^ |
| Yarn PnP | Requires packageManager field |
| Commit lockfile | Yes, for applications |
Key takeaways:
package.jsondeclares dependencies with version ranges. It is the manifest that describes the project and what it needs. It must containnameandversionand is created withnpm init.package-lock.jsonrecords exact versions. It is generated by npm and describes the exact tree that was installed. It must be committed to version control for applications .npm ciinstalls from the lockfile. It fails ifpackage.jsonand the lockfile are out of sync, guaranteeing that CI and production install exactly what was tested.- The lockfile prevents dependency drift. Without it, a fresh install could resolve to a different version than the one that was tested, introducing regressions .
- Workspaces automate local package linking. The
workspacesfield in the rootpackage.jsondefines globs for local packages.npm installat the root symlinks them intonode_modules. - pnpm uses
pnpm-workspace.yamland theworkspace:protocol. The protocol ensures that a local package is used rather than a registry version, preventing accidental installs from the registry . - Yarn Modern supports the
workspace:protocol, but Yarn Classic does not. Set thepackageManagerfield inpackage.jsonso Corepack uses the correct version and tools like Renovate do not fail . - Commit the lockfile for applications; for libraries, it is optional. Applications need reproducibility; libraries typically let downstream users resolve their own versions.
Remember: package.json is the project’s manifest, declaring what it is and what it needs. package-lock.json is the project’s record of exactly what was installed, making installs reproducible across machines and CI runs. Workspaces extend this model to repositories with multiple packages, automatically linking local packages so they can be imported by name. The lockfile is the foundation of reproducible builds: commit it, use npm ci in CI, and never delete it to fix an install problem. Workspaces make monorepos practical, but they also make the lockfile more important, because a single root lockfile covers every package in the workspace. Understanding these three files is understanding how Node.js projects manage dependencies.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!