How to check unused dependencies in package json
Run npx depcheck in the project root. It lists every package in package.json that no file imports. In the project below it named three, and one of them is genuinely unused: dotenv is loaded by a node flag and postcss-nested by a config key, and depcheck reads neither.
Why check this
Dependency cleanup runs before a release, or whenever an audit report is long enough that somebody proposes deleting entries. The tool that produces the delete list is the thing to verify first.
The failure it prevents is a deploy that dies at process start. Remove dotenv on the tool's word and node -r dotenv/config src/index.js throws Cannot find module before a single line of application code runs. Local tests that call functions directly never touch that script, so the pipeline stays green until the service starts.
Prerequisites
- Node 22 and npm 10. The outputs below came from npm 10.9.8 on Node 22.23.2 and depcheck 1.4.7.
- The depcheck README for the flag list.
- A scratch project, not your repository, so the findings are small enough to check by hand. These six files are the whole of it.
{
"name": "depdemo",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node -r dotenv/config src/index.js",
"build": "node build.js"
},
"dependencies": {
"dayjs": "1.11.13",
"dotenv": "16.4.7",
"lodash": "4.17.21",
"postcss": "8.4.49",
"postcss-nested": "7.0.2"
}
}
// build.js
const fs = require('fs');
const postcss = require('postcss');
const config = require('./postcss.config.cjs');
const plugins = Object.keys(config.plugins).map((name) => require(name)(config.plugins[name]));
const css = fs.readFileSync('src/app.css', 'utf8');
postcss(plugins)
.process(css, { from: 'src/app.css', to: 'dist/app.css' })
.then((result) => {
fs.writeFileSync('dist/app.css', result.css);
console.log(result.css);
});
// postcss.config.cjs
module.exports = {
plugins: {
'postcss-nested': {},
},
};
// src/index.js
const dayjs = require('dayjs');
console.log(process.env.STAMP_LABEL, dayjs().format('YYYY-MM-DD'));
// src/log.js
const pc = require('picocolors');
module.exports = (msg) => console.log(pc.green(msg));
/* src/app.css */
.card { color: #111; .title { font-weight: 700; } }
One more file, .env, holds the single line STAMP_LABEL=nightly. Run npm install, then mkdir dist.
Steps
- Step 1.
Prove the build uses the two packages that are about to be called unused.
npm start && npm run build> depdemo@1.0.0 start > node -r dotenv/config src/index.js nightly 2026-09-12 > depdemo@1.0.0 build > node build.js .card { color: #111; } .card .title { font-weight: 700; }nightlycame from.env, so dotenv ran. The nested.titlerule was flattened into.card .title, so postcss-nested ran. - Step 2.
Run the tool.
npx --yes depcheck@1.4.7Unused dependencies * dotenv * lodash * postcss-nested Missing dependencies * picocolors: .\src\log.jsThree names in the first list, one of which step 1 measured as unused:
lodash. The second list is the more interesting half, and it is the one people skip. - Step 3.
Read the exit code before wiring this into a pipeline.
npx --yes depcheck@1.4.7 >/dev/null 2>&1; echo "depcheck exit $?"depcheck exit 127127 with three findings, 127 with one, and 0 when nothing is flagged. The code does not count anything.
- Step 4.
Ask which file proved each package used, rather than trusting the summary.
npx --yes depcheck@1.4.7 --json | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);console.log('using:',Object.keys(j.using).join(', '));console.log('unused:',j.dependencies.join(', '));console.log('missing:',Object.keys(j.missing).join(', '));})"using: postcss, dayjs, picocolors unused: dotenv, lodash, postcss-nested missing: picocolorsusingholds only the three names reached through arequirecall. Nothing in the JSON records a package script or a config key, so nothing there could have found the other two. - Step 5.
Silence the two false positives and see what is left.
npx --yes depcheck@1.4.7 --ignores="dotenv,postcss-nested"Unused dependencies * lodash Missing dependencies * picocolors: .\src\log.jsOne package to delete and one to declare. That is the real result of the check, and it took step 1 to get here.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| A name under Unused dependencies | No file in the scanned tree imports it | Search the repository for the name as a plain string before deleting it. |
| A name under Missing dependencies with a file | That file imports a package package.json does not declare | Declare it. It resolves today only because something else installed it. |
| No depcheck issue | Both lists are empty for the current flags | Confirm the --ignores list is still justified. |
| Exit code 127 | At least one finding exists | Read the JSON. The code is the same for one finding and for twenty. |
Common mistakes
What to check next
- How to check why a package is installed: names the requester behind a package the missing list reports.
- How to check dependency tree npm: shows whether the package you are about to delete has other requesters.
- How to check if package lock is in sync: run it after removing anything, so the lockfile records the removal.
- How to check npm packages for vulnerabilities: the other reason a dependency list gets shortened.
FAQ
How do I check unused dependencies?
npx depcheck in the project root. Treat its output as candidates. Confirm each one by searching the repository for the package name as a plain string, including package scripts and config files.
Does it check devDependencies too?
Yes, under a separate devDependencies heading. The project above declares none, so that section was empty in every run on this page.
How do I suppress a name I have confirmed?
--ignores="dotenv,postcss-nested", comma separated. Keep the reason next to the flag in your pipeline file, because the next reader cannot tell a confirmed exception from an old one.
Can I fail a build on the result?
Run it with --json and decide on the dependencies and missing keys. The exit code is 127 for any finding and 0 for none, which is too coarse and collides with the shell code for a missing command.
Verified
Verified by Maks Vernydepcheck 1.4.7npm 10.9.8Node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate8 minpublished updated Maks Verny