Every npm install is a decision with a 2-year half-life. The package you add today will appear in your bundle, your lockfile, your security audits, and your upgrade path for years. As Russ Cox put it, adding a dependency means "outsourcing the design, writing, testing, debugging, and maintenance of code to strangers on the internet."
Most developers evaluate packages by checking the README, the star count, and whether the first code example works. Then, 18 months later, the package is unmaintained, a vulnerability is discovered, and the migration to an alternative touches 40 files. This has happened at scale: left-pad (11 lines, broke the entire npm ecosystem when unpublished), event-stream (maintainer handed publish rights to a stranger who injected cryptocurrency-stealing malware), and xz-utils (a 3-year social engineering campaign that planted a backdoor in SSH authentication, discovered by accident when someone noticed 500ms of latency).
This prompt forces a structured evaluation before the install, not after the regret.
The prompt
I'm considering adding [package name] to this project. Evaluate whether it's a good decision.
## 1. Do we actually need it?
Before evaluating the package, answer these questions:
- What specific problem does this package solve for this project?
- Can we solve it with what we already have? (existing dependencies, built-in APIs, native language features, platform APIs like Intl, crypto, streams)
- How much code would a minimal custom implementation take? If it's under ~50 lines and the edge cases are manageable, a dependency is probably not worth it.
- Is this a one-time use or will it be used throughout the codebase?
If the answer is "we don't need it" or "a 20-line utility function would suffice," stop here. Don't evaluate the package.
## 2. Package health
Check these indicators:
- **Maintenance**: When was the last release? Last commit? Are issues being responded to, or is the issue tracker a graveyard?
- **Stability**: What major version is it on? How frequently do major versions ship? (Frequent major versions = frequent migration work for you.)
- **Bus factor**: How many active maintainers? Is it a solo project or backed by a team/company? Has maintainer ownership changed recently? (Maintainer handoff is the #1 vector for supply chain attacks — event-stream, xz-utils.)
- **Downloads**: npm weekly downloads and trend (growing, stable, declining). Trend matters more than absolute number.
- **License**: Is it compatible with this project's license? (MIT, Apache-2.0 are safe. GPL, SSPL, BUSL require careful evaluation.)
- **Install scripts**: Run `npm view [package] scripts`. Does it define preinstall or postinstall scripts? Unexpected install scripts are the primary vector for supply chain attacks.
## 3. Technical fit
Evaluate the API and implementation:
- **API surface**: How large is the public API? A package with 3 functions that do exactly what you need is better than one with 100 functions where you use 2.
- **Bundle size**: Check the package size (bundlephobia.com). Does it tree-shake? Will it add significant weight for the functionality you're using?
- **Transitive dependencies**: A package with 2 direct dependencies might pull in 30 transitive ones. Each is a potential vulnerability, upgrade burden, and supply chain risk. Check `npm ls [package]` after a trial install to see the full tree. A package with 0 dependencies of its own is a strong positive signal.
- **TypeScript**: Does it ship types? Are they accurate or community-maintained @types?
- **Integration**: Does it conflict with anything already in the project? (duplicate functionality, CSS framework conflicts, incompatible peer dependencies)
- **Escape hatch**: If you need to remove this package later, how hard is it? Is usage concentrated behind a thin wrapper (easy to replace) or spread across 40 files (long-term commitment)?
## 4. Alternatives
Evaluate at least 2 alternatives (including "build it yourself"):
- **Alternative A**: [another package that solves the same problem]
- **Alternative B**: Custom implementation
For each, compare on: bundle size, API simplicity, maintenance health, dependency count, and how many files in this project would use it.
## 5. Recommendation
Based on the above:
- **Install it** — if it solves a real problem, is well-maintained, reasonably sized, and has good escape hatch properties
- **Build it yourself** — if the custom implementation is small, the package is heavy for what you need, or the package has maintenance concerns
- **Defer the decision** — if you're unsure about the requirement. Don't install preemptively.
If recommending installation, also suggest:
- Whether to wrap it in a thin adapter (for easier future replacement)
- Whether to pin the version or use a range
Quick evaluation checklist
Before running the full prompt, this 60-second check covers the most important signals:
npm view [package] scripts— check for unexpected install scripts (10 sec)- Bundlephobia — check size and tree-shakeability (15 sec)
- Snyk Advisor — health score, maintenance activity, known vulns (15 sec)
- Socket.dev — supply chain risk signals (maintainer changes, obfuscated code, network access) (15 sec)
npm pack [package] --dry-run— inspect what's actually in the package (10 sec)
If any of these raise a red flag, run the full evaluation prompt.
Example
Evaluating whether to add date-fns to a project that currently uses raw Date:
### 1. Do we actually need it?
Date formatting appears in 3 components (post dates, RSS feed, sitemap).
Currently using toISOString() and manual formatting. The pain point:
parsing ISO strings and formatting "February 22, 2026" requires ~15 lines
of Intl.DateTimeFormat code per format.
Alternative: Intl.DateTimeFormat is built-in and handles all current
formatting needs. No package required.
### 5. Recommendation
**Build it yourself.** A 10-line wrapper around Intl.DateTimeFormat covers
all current use cases. date-fns adds 5KB gzipped for functionality the
platform provides natively. If date manipulation (add days, diff months)
is needed later, revisit this decision.
When to use it
- Before running
npm installfor any package you'll import in production code - When choosing between competing packages for the same problem
- When a team member proposes adding a dependency and you want a structured evaluation
- During architecture reviews when evaluating the technology stack
When NOT to use it
- For dev dependencies with no production impact (prettier, eslint plugins, testing utilities) — the calculus is different because bundle size and runtime risk don't apply
- For framework-level decisions (React vs Vue, Next.js vs Remix) — those are architectural decisions, not dependency evaluations
- When the package is already installed and you're evaluating whether to keep it — use the Dependency Audit prompt instead
The 50-line rule
Before evaluating any package, ask: could I write this in 50 lines of code?
If yes, write it yourself. You get:
- Zero dependencies — no supply chain risk, no version conflicts
- Exact API — tailored to your use case, no unused surface area
- Full understanding — you wrote it, you can debug it
- No upgrades — no breaking changes from upstream, ever
The threshold sits at an inflection point: below 50 lines, you're dealing with pure utility functions where the maintenance cost of your own code is lower than the overhead of tracking, upgrading, and securing a dependency. Above 50 lines, you're likely re-implementing something with tricky edge cases (timezone arithmetic, cryptography, HTML parsing) where a well-tested library genuinely adds value.
The caveat: count the test code too, not just the implementation. If your 30-line utility needs 100 lines of tests to cover edge cases you'd get for free from a library, the library might still win. The question is whether the edge cases are real for your use case or theoretical.
Left-pad was 11 lines. is-number was 1 line. is-positive-integer was 4 lines. Every one of these had thousands of dependents and should have been written inline. The 50-line rule would have prevented all of them.
Tips
- The "escape hatch" evaluation in Section 3 is the most underrated factor. A package used in 2 files behind a wrapper is trivial to replace. A package whose API is spread across 40 files is a long-term commitment whether you want it to be or not.
- Weekly download trends matter more than absolute numbers. A package with 10K downloads/week and growing is healthier than one with 1M downloads/week and declining.
- The Sindresorhus counterargument: even trivial packages benefit from shared implementations that handle cross-environment edge cases (is-number handles NaN, Infinity, numeric strings). Consider whether those edge cases actually apply to your code before dismissing this argument.
- When wrapping a dependency in an adapter, the wrapper should expose only the functionality you actually use. If you use 3 of a library's 50 functions, your wrapper has 3 functions. This makes the replacement surface explicit.