npm Dependencies: How to Evaluate a Library Before Shipping It to Production
Back in 2005, when I was 16 and managing the network at a cyber café, I learned something no manual ever taught me: every cable you plugged in was debt. If the vendor for that cable disappeared or changed the connector, the problem was yours. Not the vendor's, not the customer's. Yours. Today, when I look at a package.json with 180 direct dependencies in a TypeScript project, I think exactly the same thing. Every entry in that file is a cable someone is going to have to maintain. And in most cases, that someone is you.
My take is direct: adding an npm dependency isn't just installing code — it's assuming its maintenance, its CVE history, its transitive dependencies, and the exit cost when the library gets abandoned. The question isn't "does it work?" The question is "what happens when it stops working in six months?"
Why Evaluating npm Dependencies Is a Maintenance Decision, Not Just a Security One
The official npm documentation defines a package as "a file or directory described by a package.json" (npm docs). That's all npm guarantees as a platform: that the file exists and has metadata. Nothing about whether the author is still active, whether it has tests, whether the types are correct, or whether you'll be able to upgrade in two years without breaking half the system.
What the official docs don't say — and where people get burned — is that a published package can freeze in time. The author might not have bandwidth, might abandon the project, or might simply never hear about a relevant CVE. And at that point, the debt is yours.
There are three dimensions that matter before installing anything in a TypeScript project with pnpm:
- Active maintenance: When was the last commit? Are there PRs that have gone unanswered for months? Any releases in the past year?
-
Attack surface and types: Does the package ship its own types (
@types/) or generate them? How many transitive dependencies does it drag in? - Exit cost: If you need to rip it out tomorrow, how much of your own code changes?
How to Audit a Dependency Before pnpm add
The usual recipe is: search on npm, check if it has GitHub stars, install it, done. The problem is that measures popularity, not quality or longevity. Popularity and active maintenance are not the same thing.
Here's the process I use, step by step and fully reproducible:
1. Check the Real State of the Repository
Before installing, open the repo on GitHub and look at:
-
Last commit on
main: if it's been more than 12 months with no activity and it's not a stable utility library (likelodash), that's a signal. - Open issues: Are there bugs sitting unanswered for months? CVEs mentioned but not patched?
- CHANGELOG or releases: a serious project has a version history. If it doesn't, the risk surface goes up.
2. Analyze Transitive Dependencies with pnpm why
# Install in an isolated test project
pnpm add <package-name>
# See what it brought along
pnpm why <package-name>
# Or a full dependency tree
pnpm list --depth=3
A dependency that looks small can drag in 40 transitive packages. That's not automatically bad — but if two of those 40 have active CVEs, the problem is yours even if your own code never calls them directly.
3. Run a Security Audit From the Start
# Basic audit with npm (works on pnpm projects too)
npm audit
# To see only critical and high vulnerabilities
npm audit --audit-level=high
# If you want the JSON to process it
npm audit --json | jq '.vulnerabilities | to_entries[] | select(.value.severity == "critical")'
npm audit uses the npm Advisory Database to cross-reference installed versions against known CVEs. It's not infallible — there are vulnerabilities that don't have an advisory yet — but it's the minimum reasonable floor before committing to a dependency.
4. Verify TypeScript Types
In a TypeScript project, a dependency without types is guaranteed friction. Check:
# Does the package ship its own types?
cat node_modules/<package>/package.json | grep '"types"'
# Are @types/ available?
npm info @types/<package>
If the package doesn't ship its own types and the @types/ are community-maintained (not by the original author), you have two separate sources of drift. When the package updates and @types/ doesn't, the compiler fails in ways that aren't obvious.
5. Evaluate Exit Cost With Your Own Interface
This is the step that gets skipped the most. The question isn't just "does it work today?" but "how much code do I change if I rip it out tomorrow?"
// Pattern that reduces exit cost:
// Wrap the dependency behind your own interface
// ❌ Using the dependency directly throughout the codebase
import { parse } from 'some-date-lib'
const date = parse('2025-01-15')
// ✅ Abstracting behind your own module
// lib/dates.ts
import { parse as _parse } from 'some-date-lib'
export function parseDate(input: string): Date {
return _parse(input) // single entry point
}
If the library is spread across 40 different files with no abstraction, removing it costs a major refactor. If it lives in one module, removing it costs an internal implementation swap.
The Most Common Mistakes When Evaluating npm Dependencies
Mistake 1: Confusing Weekly Downloads With Stability
npm download numbers include automatic mirrors, CIs, and pipelines. A library with 2M weekly downloads can have a maintainer who hasn't merged a PR in a year. Downloads are a lagging indicator of past popularity, not a guarantee of future support.
Mistake 2: Ignoring devDependencies in Projects With Build Steps
If a vulnerable devDependency is involved in the build (babel, webpack, esbuild, tsx), the code it generates can be compromised. The devDependencies field in package.json separates intent, not risk. If it goes through the compiler, it matters.
Mistake 3: Not Looking at peerDependencies
# Check what versions of React/Node the lib expects
npm info <package> peerDependencies
A library that asks for React 17 as a peer in a React 19 project might work — or it might produce silent bugs from duplicate context. Peer conflicts are one of the most common hidden costs in stack upgrades.
Mistake 4: Assuming a Small Package Is Safe
Attack surface isn't proportional to size. The event-stream incident in 2018 showed that a small utility package, transferred to a new maintainer, can become an attack vector. Small doesn't mean harmless. (Source: npm blog on the incident)
This kind of risk connects directly to what I wrote about OAuth Scope Creep: attack surface accumulates at the edges, not the center.
Decision Matrix: Do I Add This Dependency or Not?
| Criterion | Green (add it) | Yellow (evaluate further) | Red (avoid or wrap) |
|---|---|---|---|
| Last release | < 6 months | 6–18 months | > 18 months with no activity |
| TypeScript types | Bundled in the package | Active @types/ aligned with the package |
No types or outdated @types/
|
| Active CVEs | None | Low severity, no public exploit | Critical or high with no patch |
| Transitive deps | < 10 | 10–40 | > 40 or deps with CVEs |
| Exit cost | Easy to wrap | Moderate coupling | Invasive across multiple modules |
| Active maintainer | Responds to issues/PRs | Slow but responds | No visible activity |
If a dependency lands in "Red" on more than two criteria, the right question is: do I actually need this abstraction, or can I implement the specific logic I need in 50 lines of my own code?
When working on projects with pnpm workspaces — like I described in the post about pnpm workspaces and CI on Railway — this evaluation matters twice as much: a problematic dependency in a shared package of the monorepo gets inherited by every app in the workspace.
What This Checklist Can't Guarantee
Being honest about the limits:
- It doesn't predict future abandonment: a library with recent releases can get abandoned tomorrow. The checklist measures current state, not future state.
-
npm auditdoesn't cover all vectors: business logic vulnerabilities, sophisticated supply chain attacks, and unreported CVEs don't show up in a standard audit. It's the floor, not the ceiling. - The real exit cost only gets measured in practice: estimating the cost of removing a dependency is a heuristic. Until you actually do it, it's a projection. If the project already has the dependency deeply integrated, the retrospective evaluation is more expensive than the prospective one.
- GitHub metrics are indicators, not proof: an archived repository can be stable because it reached feature-complete. A repo with tons of commits can be unstable due to constant refactoring. Context matters.
FAQ: Evaluating npm Dependencies in TypeScript Projects
How many direct dependencies is "too many" in a TypeScript project?
There's no universal number. What is a warning sign is having more than 50–60 direct dependencies without having actively evaluated which ones could be replaced by your own implementations. The criterion isn't the count — it's whether every entry in dependencies has a clear reason that can't be solved in fewer than 100 lines of your own code.
Does pnpm have security advantages over npm or yarn for this kind of audit?
pnpm has a different storage model (content-addressable store) that avoids duplication and makes the dependency tree more predictable. But for CVE audits, npm audit is still the standard tool and works with any lockfile. pnpm's advantage in this context is more about tree predictability than intrinsic security.
What do I do if a dependency has a CVE but no fix is available?
First, evaluate whether the CVE applies to how you're using it. Many CVEs have specific exploitation conditions that may not apply to your context. If it does apply, look for a fork with the fix, replace the dependency, or implement the minimum necessary functionality yourself. Keeping the vulnerable dependency and "noting it for later" is the most comfortable path and the most expensive one in the medium term.
Does it make sense to evaluate devDependencies with the same rigor?
Less rigor, but not zero. Build tools, linters, and compilers that go through the CI pipeline deserve a basic review. A devDependency that's only used on a local machine has less urgency than one that participates in generating the artifact going to production.
How do I evaluate a dependency when it has no visible public repository?
If an npm package doesn't have a public repository linked and has more than a couple of months of existence, the default criterion is don't install it in a serious project. The absence of a public source doesn't imply malice, but it eliminates the possibility of a code audit. Without visible source, the analysis is limited to what the package declares in its package.json — which is incomplete information.
How does this affect maintaining a monorepo with multiple apps?
A problematic dependency in a shared/ package of the monorepo propagates automatically to all consumers. That makes upfront evaluation more important, not less. The cost of a CVE or a breaking change in a shared dependency multiplies by the number of apps in the workspace. It's worth spending more time on shared package dependencies than on ones specific to a single app.
My Take and the Next Concrete Step
Adding an npm dependency is a technical decision with consequences that extend far beyond the current sprint. I'm not saying you should avoid libraries — that would be absurd in an ecosystem where composition is the model. I'm saying the upfront evaluation costs half an hour and can prevent weeks of maintenance debt.
What I don't buy is the idea that a package's popularity is sufficient evidence to install it without further analysis. GitHub stars don't pay the cost of a migration when the library goes unsupported.
What I do buy: implementing your own logic when the dependency alternative drags in 30 transitives, has no types, or has a maintainer who hasn't responded in a year. In those cases, 80 well-tested lines of your own code are a more honest investment than delegating to a package you can't control.
The next concrete step: open the package.json of the most active project you're currently working on. Pick the five dependencies you know the least about. Run pnpm why <package> on each one and look at the repository on GitHub. In at least one of them, you'll find something that deserves a conversation about whether it's still worth keeping.
Original sources:
- npm package documentation: https://docs.npmjs.com/about-packages-and-modules
- npm blog — event-stream incident (2018): https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident
This article was originally published on juanchi.dev
Top comments (0)