Production engineering guide for dependency management covering patterns, implementation strategies, and operational best practices.
Dependency management is the process of tracking, resolving, and maintaining software components that your project requires to build, run, or test. It matters when you are building any software system with external libraries, frameworks, or services, especially in environments where version conflicts, transitive dependencies, or breaking changes can cause silent failures or runtime errors.
To add a new dependency to a project using a standard package manager, run:
npm install [email protected]
The @ version specifier is required for installing a specific version. Without it, the latest version will be installed.
If you want to add a dependency as a development dependency, use:
npm install --save-dev [email protected]
This adds the dependency to the devDependencies section in package.json.
When multiple dependencies require different versions of the same package, the package manager resolves this by installing a compatible version or nesting dependencies.
To see dependency resolution details:
npm ls express
This shows all installed versions of express and their relationships. If there's a conflict, you'll see something like:
[email protected]
└── [email protected]
└── [email protected]
To force a specific version across all dependencies:
npm install [email protected] --save-exact
This saves the exact version in package.json without a caret (^) or tilde (~) prefix.
To remove a dependency:
npm uninstall express
To remove a dev dependency:
npm uninstall --save-dev jest
If you remove a dependency that is also used as a transitive dependency by another package, it won't be removed from node_modules unless no other package requires it.
In a Dockerfile, dependencies are typically installed during the build phase using package managers like apt-get, yum, or npm.
Example for a Node.js application:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
The command npm ci ensures deterministic installs from a package-lock.json file, unlike npm install.
If you use npm install instead of npm ci, it may not respect the exact versions in package-lock.json, leading to inconsistent builds across environments.
To update dependencies in a containerized environment:
docker run --rm -v $(pwd):/app node:18-alpine sh -c "cd /app && npm install express@latest"
Then commit the updated package-lock.json and rebuild the image.
If you update dependencies without updating the lock file, the containerized build will not reflect the new versions.
To run tests in an environment that mirrors production dependencies:
npm test
This command typically runs a script defined in package.json under the "test" key. If your test setup requires specific versions of dependencies, ensure these are specified in package.json.
If you're using Jest and want to run tests with coverage:
npm run test -- --coverage
The double dash (--) is required to pass flags to the underlying test runner.
Add test-specific dependencies like:
npm install --save-dev mocha chai sinon
These dependencies should be added to devDependencies and not included in production builds. If they are accidentally included in production, they may increase image size or introduce unnecessary runtime overhead.
In Node.js applications, ensure that your application loads the correct versions of dependencies by using a lock file:
npm install --package-lock-only
This command updates package-lock.json without installing new packages. If you are deploying to production, always use npm ci to install from the lock file.
If a dependency is not present in package-lock.json, it may resolve to an unexpected version at runtime.
To inspect transitive dependencies:
npm ls lodash
This command shows how lodash is used throughout your dependency tree. If a transitive dependency changes, it can cause silent failures without any direct change in your code.
If you rely on a transitive dependency's behavior and do not explicitly depend on it, your code may break when that transitive dependency is updated.
You can use environment variables to override dependencies:
NODE_ENV=production npm install
This affects how some packages behave during installation or runtime. For example, some packages may install additional development tools in a development environment but skip them in production.
If you are deploying to different environments, always verify that the installed dependencies match across environments using npm ci with package-lock.json.
Some packages have platform-specific binaries:
npm install node-sass
This package may fail to install if the target platform does not have compatible binaries. If you encounter an error like:
node-sass@latest: Cannot download "https://github.com/sass/node-sass/releases/download/v7.0.3/linux-x64-108_binding.node"
You should either specify a compatible version or install with the --force flag to bypass platform checks.
To check for known vulnerabilities:
npm audit
This command analyzes your dependency tree and reports any known security issues.
If vulnerabilities are found, run:
npm audit fix
This attempts to update dependencies to fix vulnerabilities. If audit fix cannot resolve all issues, it will suggest manual fixes.
Sometimes, npm audit fix fails to resolve all issues. In such cases, manually edit package.json to update vulnerable packages to versions that address the security concerns.
You can also run:
npm audit fix --force
This forces updates even when they might introduce breaking changes.
If you are deploying a project with audit failures, consider documenting which vulnerabilities were not fixed and why.
In a CI pipeline, always use npm ci instead of npm install to ensure reproducible builds:
- name: Install dependencies
run: npm ci
If your pipeline uses npm install, it may install different versions than those used in development, leading to inconsistent behavior.
To cache dependencies between CI runs:
- name: Cache node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
If you do not cache dependencies, your CI pipeline will reinstall all packages every time, increasing build time.
Adding a dependency that is already present in the tree may silently override it. For example:
npm install [email protected]
If lodash was already required by another package at version 4.17.20, and this version is compatible, no error occurs.
However, if a newer version introduces breaking changes, your application may fail without clear indication that it's due to an overridden dependency.
If package-lock.json or yarn.lock is not committed to version control, different developers may install different versions of dependencies. This leads to inconsistent builds and potentially broken functionality in production.
npm install vs npm ciUsing npm install instead of npm ci in CI environments can lead to inconsistent dependency resolution. npm install will update the lock file if it finds newer compatible versions, while npm ci strictly installs based on the lock file.
Not updating dependencies regularly can lead to accumulated security vulnerabilities or compatibility issues with new Node.js versions. Always review dependency updates before merging them into your codebase.
Keep track of changes to dependencies by monitoring package.json and package-lock.json:
git diff package.json package-lock.json
This helps identify when dependencies are added, removed, or updated.
When a major version update introduces breaking changes, always test thoroughly. For example:
npm install [email protected]
May break existing code that relies on APIs available in the previous version of Express.
If you encounter an error like:
TypeError: express.Router is not a function
It indicates that your application expects the old API from Express 4, but now uses the new API from Express 5.
If you see an error like:
Cannot find module 'express'
Ensure that express is present in package.json and that npm install or npm ci was run successfully. Also check that your deployment environment has access to the same dependency versions.
If you encounter an error like:
Error: Cannot find module 'lodash'
even though lodash is in your package.json, it may be a transitive dependency conflict. Run:
npm ls lodash
To see where lodash is being pulled from and how it's being used.
If your application works locally but fails in production, check that:
package-lock.json file is committed.npm ci in production builds.This ensures that all dependencies are installed with the exact same versions across environments.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy