| |

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

FieldPurpose
namePackage name (required)
versionSemver version (required)
descriptionPackage description
mainEntry point
typemodule for ESM, commonjs for CJS
scriptsRunnable commands
dependenciesRuntime dependencies
devDependenciesDevelopment dependencies
workspacesLocal package globs
packageManagerPins package manager version

package-lock.json Facts

FactDetail
Generated bynpm install
CommittedYes, for applications
Used bynpm ci for reproducible installs
ContainsExact versions, URLs, integrity hashes
lockfileVersion 3npm 9+

Workspaces Commands

CommandPurpose
npm install at rootLinks all workspaces
npm install pkg -w <workspace>Add dependency to workspace
npm run test --workspace=aRun script in one workspace
npm run test --workspacesRun script in all workspaces

Workspace Protocol (pnpm)

SpecifierMeaning
workspace:*Link any local version
workspace:^Link if local satisfies ^
workspace:~Link if local satisfies ~
workspace:1.5.0Link 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

PitfallWhy It HappensFix
npm ci failsLockfile out of sync with package.jsonRun npm install locally and commit lockfile
Different versions on machinesLockfile not committedAlways commit package-lock.json
Workspace not resolvingNot run npm install at rootRun npm install to symlink
workspace: protocol failsYarn Classic used instead of ModernSet packageManager field
Phantom dependencyTransitive dep hoisted to topUse pnpm or check imports
Lockfile conflicts in gitMultiple developers installing different versionsResolve 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

ItemValue
package.jsonProject manifest with name, version, dependencies, scripts
package-lock.jsonExact resolved dependency tree, integrity hashes
Lockfile purposeReproducible installs across environments
npm ciInstalls from lockfile, fails if out of sync
workspacesLocal packages linked automatically
Workspace commandnpm install pkg -w <workspace>
pnpm workspace configpnpm-workspace.yaml
pnpm protocolworkspace:*, workspace:^
Yarn PnPRequires packageManager field
Commit lockfileYes, for applications

Key takeaways:

  • package.json declares dependencies with version ranges. It is the manifest that describes the project and what it needs. It must contain name and version and is created with npm init .
  • package-lock.json records 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 ci installs from the lockfile. It fails if package.json and 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 workspaces field in the root package.json defines globs for local packages. npm install at the root symlinks them into node_modules .
  • pnpm uses pnpm-workspace.yaml and the workspace: 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 the packageManager field in package.json so 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!