Essential Libraries Basics
10 examples for how we pick, pin, and sunset npm dependencies on Node.js 24 TypeScript backends - 7 basic and 3 intermediate.
Prerequisites
- Node.js 24.18.0 (Active LTS) and npm 10+.
- One package manager per repo with a committed lockfile - see Package Managers Basics.
- Team ADR template for adding or removing runtime dependencies.
Basic Examples
1. Default Essential Stack
Start new services with this curated set before evaluating alternatives.
| Concern | Default | When to reconsider |
|---|---|---|
| Validation | zod | JSON Schema-only Fastify routes with no shared types |
| Logging | pino | Cloud provider mandates another format (rare) |
| HTTP client | got or axios | Simple outbound calls with built-in fetch |
| Dates/timezones | Luxon | Pure date math without IANA zones |
| Queue + cache | BullMQ + ioredis | SQS-only AWS shops with no Redis |
- Document the default stack in
CONTRIBUTING.mdso PRs do not re-debate basics weekly. - Deviations require a one-paragraph ADR comment in the PR.
- See dedicated pages: zod, pino, got / axios.
Related: Essential Libraries Best Practices - selection checklist
2. Evaluation Checklist Before Adding a Dependency
Score candidates before npm install.
## Dependency evaluation: <package-name>
- [ ] Last publish < 12 months OR explicit LTS maintainer
- [ ] Open issues responded within ~2 weeks for security reports
- [ ] Weekly downloads stable or growing (not abandoned spike)
- [ ] TypeScript types built-in or DefinitelyTyped quality checked
- [ ] No install scripts unless reviewed (supply chain risk)
- [ ] Transitive count acceptable (npm ls <pkg> --all)
- [ ] License compatible (MIT/Apache-2.0 typical)- Reject packages with unmaintained security advisories and no fork path.
- Run
npm auditafter install; block high severity without exception ticket. - Prefer packages the team already operates in another service - operational familiarity counts.
3. Pin Majors, Trust the Lockfile
package.json expresses intent; the lockfile expresses reality.
{
"dependencies": {
"zod": "^3.24.0",
"pino": "^9.0.0",
"ioredis": "^5.4.0"
},
"engines": {
"node": "24.18.0"
}
}npm install # updates lockfile locally with intent
npm ci # CI installs exact lockfile - never drift- Use caret on libraries you upgrade via Renovate grouped PRs.
- Pin exact versions only for known-problematic packages (temporary).
- Never commit without updating the lockfile when dependencies change.
Related: Lockfiles & Reproducible Installs
4. Prefer Built-ins Before npm
Node 24 reduces third-party surface for common tasks.
// Built-in fetch - no got/axios required for simple GET
const res = await fetch("https://api.example.com/health");
const body = await res.json();
// Built-in test runner - no jest required for small services
import { test } from "node:test";
import assert from "node:assert/strict";
test("health shape", () => {
assert.equal(typeof body.status, "string");
});- Add got or axios when you need retry policies, agents, or interceptors at scale.
- Add jest or vitest when you need mocking ecosystems beyond
node:test. - Fewer dependencies means fewer audit failures and faster
npm ci.
Related: Built-in APIs Basics
5. One Library Per Concern
Duplicate date libraries or HTTP clients create inconsistent behavior and bloated images.
# Audit overlap before approving a PR
npm ls moment date-fns luxon dayjs 2>/dev/null
npm ls axios got undici node-fetch 2>/dev/null| Bad overlap | Fix |
|---|---|
moment + luxon | Pick Luxon for IANA zones; remove moment |
axios + got | Standardize per service; shared internal SDK if needed |
winston + pino | Pino only for JSON services |
- Enforce in code review: "Does this PR introduce a second library for the same job?"
- Shared internal packages wrap the chosen lib so services do not import three HTTP stacks.
6. Document Why in package.json Comments (ADR Link)
Tie unusual dependencies to a decision record.
{
"dependencies": {
"bullmq": "^5.0.0",
"ioredis": "^5.4.0"
}
}<!-- docs/adr/003-job-queue.md -->
# ADR 003: BullMQ for async jobs
- Status: accepted
- Context: Need delayed jobs, retries, DLQ with Redis already in stack
- Decision: BullMQ over raw Redis lists
- Consequences: Operate Redis HA; monitor queue depth- ADRs live in
docs/adr/and are linked from PR descriptions. - Revisit ADRs when the package has a major security incident or maintainer change.
7. Security and Supply Chain Gates
Treat new dependencies as production risk, not convenience.
npm audit --audit-level=high
npx socket npm audit # optional: proactive supply chain scan# .github/workflows/pr-checks.yml (excerpt)
- run: npm ci
- run: npm audit --audit-level=high- Block merge on unmitigated high/critical advisories in direct or transitive deps.
- Review
postinstallscripts in lockfile diffs - they run on every developer laptop. - Enable provenance for internal published packages.
Related: Supply Chain: npm audit & Socket
Intermediate Examples
8. Sunset a Library (Deprecation Window)
Remove dependencies with a migration plan, not a big-bang delete.
## Sunset plan: winston → pino
Week 1: ADR accepted; no new winston imports (lint rule)
Week 2-3: Migrate high-traffic services; dual-log if needed
Week 4: Remove winston from package.json; npm audit confirms gone// Transitional adapter - delete after migration
import pino from "pino";
export const log = pino({ level: process.env.LOG_LEVEL ?? "info" });- Announce in
#backendwith affected services list. - Track progress in a GitHub issue with checkboxes per repo.
- Remove from org-wide Renovate allowlist when complete.
9. Version Alignment Across Monorepo Services
Shared libraries force consistent transitive versions.
{
"name": "@acme/shared-validation",
"dependencies": {
"zod": "^3.24.0"
},
"peerDependencies": {
"zod": "^3.24.0"
}
}- Workspace packages export Zod schemas; services import
@acme/shared-validation. - One Renovate group bumps
zodacross allpackage.jsonfiles weekly. - Run
npm run typecheckat repo root after grouped upgrades.
Related: Workspaces & Monorepos
10. When to Say No to a Dependency
Decline packages that fail the team bar even if they are popular on npm.
| Reject when | Example | Alternative |
|---|---|---|
| Unmaintained 2+ years | abandoned ORM wrapper | Prisma, Drizzle, raw pg |
| No TypeScript path | untyped mega-lib | thinner typed alternative |
| Heavy native addon for one function | image lib for resize | platform service or sharp with ADR |
| Duplicates platform capability | dotenv in k8s-only prod | platform inject + Zod at boot |
- "Not invented here" is wrong; "not operated here" is valid.
- Solo prototypes may experiment; production services follow the essential stack.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.