Open almost any JavaScript project created five years ago and you'll probably find hundreds or even thousands of dependencies inside node_modules. Some are essential. Others were installed because JavaScript lacked built-in capabilities at the time.
Fast forward to today, and the language has evolved dramatically.
Modern browsers, Node.js, and standardized Web APIs now provide features that previously required external libraries. Yet many projects continue to install packages out of habit, increasing maintenance costs without adding meaningful value.
This is not an argument against the npm ecosystem. It remains one of the greatest strengths of JavaScript. Instead, it is a reminder to pause before running npm install.
Sometimes the best dependency is no dependency at all.
The hidden cost of unnecessary dependencies
Adding a package seems harmless until your project grows.
A single library may pull in dozens of transitive dependencies. Those dependencies have maintainers, release cycles, vulnerabilities, breaking changes, and licenses to track.
Over time, this creates problems including:
Bloated node_modules directories
Longer installation times
Larger production bundles
More security advisories
Increased supply chain risk
Harder dependency upgrades
More frequent compatibility issues
Teams building production software often discover that dependency management consumes far more engineering time than expected.
When native APIs solve the same problem cleanly, reducing external packages becomes an easy architectural win.
1. Replace axios with fetch()
For years, Axios was the default recommendation for HTTP requests. Before widespread browser support for the Fetch API, that made perfect sense.
Today, fetch() is available in modern browsers and recent versions of Node.js, making it a capable default choice for many applications.
Before
import axios from "axios";
async function getUsers() { const response = await axios.get("/api/users");
return response.data; }
After
async function getUsers() {
// Fetch data from the server
const response = await fetch("/api/users");
// Throw if the response is unsuccessful
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Parse JSON payload
return await response.json();
}
Sending JSON data
async function createUser(user) {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(user)
});
if (!response.ok) {
throw new Error("Failed to create user");
}
return response.json();
}
When Axios still makes sense
Axios continues to offer conveniences like:
Request interceptors
Automatic JSON transformation
Older browser compatibility
Built-in timeout helpers
Richer configuration defaults
For many REST integrations, however, native fetch() is more than sufficient.
2. Replace uuid with crypto.randomUUID()
Generating unique identifiers once required external packages.
Modern JavaScript now exposes cryptographically secure UUID generation through the built-in Crypto API.
Before
import { v4 as uuidv4 } from "uuid";
const orderId = uuidv4();
After
// Generates an RFC 4122 version 4 UUID
const orderId = crypto.randomUUID();
console.log(orderId);
This produces identifiers suitable for client-side IDs, temporary objects, cache keys, and many application-level use cases.
Practical example
function createDraftPost(title) {
return {
id: crypto.randomUUID(),
title,
status: "draft",
createdAt: new Date().toISOString()
};
}
No additional dependency required.
3. Replace lodash.cloneDeep with structuredClone()
Deep copying nested objects has historically been error-prone.
Many developers relied on Lodash or even the infamous:
JSON.parse(JSON.stringify(object))
The latter silently drops dates, maps, sets, undefined values, and more.
Modern JavaScript includes structuredClone().
Before
import cloneDeep from "lodash.clonedeep";
const backup = cloneDeep(settings);
After
const backup = structuredClone(settings);
Example
const profile = {
name: "Alex",
preferences: {
theme: "dark"
}
};
const copiedProfile = structuredClone(profile);
// Modify clone only
copiedProfile.preferences.theme = "light";
console.log(profile.preferences.theme);
// dark
The original object remains unchanged.
Keep in mind
structuredClone() works well with many built-in types but cannot clone functions or DOM elements. Review your object graph before replacing existing implementations.
4. Replace query-string or qs with URLSearchParams
Parsing and constructing query strings used to require helper libraries.
Now it's built directly into the platform.
Before
import qs from "qs";
const parsed = qs.parse(window.location.search);
After
const params = new URLSearchParams(window.location.search);
const page = params.get("page");
const sort = params.get("sort");
Building URLs
const params = new URLSearchParams();
params.set("page", "2");
params.set("limit", "25");
params.set("search", "laptop");
const url = `/products?${params.toString()}`;
console.log(url);
Output:
/products?page=2&limit=25&search=laptop
This approach is readable, standards-compliant, and available across modern environments.
5. Replace simple Moment.js usage with Intl.DateTimeFormat
Moment.js transformed JavaScript date handling for years.
However, many projects only use it to display formatted dates.
If formatting is your primary requirement, the internationalization APIs built into JavaScript are often enough.
Before
import moment from "moment";
const formatted = moment().format("MMMM D, YYYY");
After
const formatter = new Intl.DateTimeFormat("en-US", {
month: "long",
day: "numeric",
year: "numeric"
});
console.log(formatter.format(new Date()));
Localized formatting
const invoiceDate = new Date();
const formatter = new Intl.DateTimeFormat("en-GB", {
dateStyle: "full",
timeStyle: "short"
});
console.log(formatter.format(invoiceDate));
Users automatically receive output tailored to their locale without additional packages.
Important trade-off
Complex timezone calculations, date arithmetic, recurring schedules, and advanced parsing may still justify dedicated libraries such as Luxon or date-fns.
6. Replace lightweight event emitter utilities with EventTarget
Custom event systems often rely on tiny packages or Node's EventEmitter abstraction.
For browser-based applications and increasingly cross-platform code, EventTarget provides a native alternative.
Example
class ShoppingCart extends EventTarget {
addItem(product) {
this.dispatchEvent(
new CustomEvent("itemAdded", {
detail: product
})
);
}
}
const cart = new ShoppingCart();
cart.addEventListener("itemAdded", (event) => {
console.log("Added:", event.detail.name);
});
This pattern integrates naturally with browser APIs while avoiding another dependency.
7. Replace object helper packages with native syntax
Several utility packages exist solely to merge or copy objects.
Most of these operations are now first-class language features.
Object spread
const defaults = {
theme: "light",
notifications: true
};
const userSettings = {
notifications: false
};
const merged = {
...defaults,
...userSettings
};
console.log(merged);
Object.assign
const target = {
role: "user"
};
Object.assign(target, {
active: true
});
console.log(target);
Combining arrays
const frontend = ["React", "Angular"];
const backend = ["Node.js", "Go"];
const stack = [...frontend, ...backend];
For many projects, these built-in capabilities eliminate the need for helper libraries whose functionality can be expressed in one line.
When third-party libraries are still the right decision
Native APIs are powerful, but avoiding dependencies should never become dogma.
A mature library remains the better choice when:
You need advanced timezone manipulation.
Your application must support legacy browsers.
Complex retry logic and request middleware are essential.
Domain-specific algorithms would take weeks to implement correctly.
Security-sensitive functionality has already been vetted by a trusted community.
You require years of backwards compatibility across multiple environments.
The goal is thoughtful dependency management, not dependency elimination.
Practical checklist before installing a package
Whenever you're tempted to add another npm dependency, ask yourself:
Can modern JavaScript already do this?
Is this feature only needed in one place?
Does the package introduce many transitive dependencies?
Will future maintainers understand why it exists?
Is the maintenance burden justified?
Does the browser or Node.js runtime already expose an equivalent API?
These questions often reveal that an installation isn't necessary.
Keeping your dependency graph lean
Engineering teams maintaining long-lived applications often adopt a few simple habits:
Audit dependencies every quarter.
Remove packages that are no longer imported.
Prefer standards-based APIs when practical.
Avoid utility packages for one-line helpers.
Keep runtime dependencies separate from development tooling.
Upgrade Node.js regularly to benefit from new platform capabilities.
Review bundle reports before adding libraries.
Small improvements accumulate into simpler builds, faster installs, and lower operational risk.
Lessons from production systems
One recurring observation across enterprise projects is that many dependencies survive simply because nobody revisits earlier decisions.
A package added in 2019 may no longer provide unique value in 2026.
Modern JavaScript continues to close historical gaps, allowing engineers to simplify codebases while relying on standardized APIs that receive ongoing browser and runtime support.
Teams delivering enterprise applications, including those offering full stack development services, API integration services, Angular development services, web portal development services, or broader web design and development services, often benefit from periodically reviewing their dependency tree. Likewise, a nodejs development company maintaining large-scale backend platforms can reduce maintenance overhead and improve long-term stability by replacing unnecessary packages with built-in platform capabilities where appropriate.
Final thoughts
The npm ecosystem remains one of JavaScript's biggest advantages, but installing a package should be an intentional engineering decision rather than muscle memory.
Before adding another dependency, check whether modern JavaScript already provides a standardized solution. Features like fetch(), crypto.randomUUID(), structuredClone(), URLSearchParams, Intl.DateTimeFormat, EventTarget, and native object utilities cover many everyday tasks with zero installation cost.
Keeping dependencies lean improves maintainability, reduces supply chain exposure, and makes projects easier to upgrade over time. The best codebase is not the one with the fewest packages. It is the one where every dependency has earned its place.
Frequently Asked Questions
1. Is fetch() a complete replacement for Axios?
Not always. fetch() handles most HTTP requests well, but Axios still offers conveniences like interceptors, richer configuration, and simplified request cancellation patterns.
2. Is crypto.randomUUID() secure enough for production?
Yes. It generates cryptographically secure Version 4 UUIDs and is suitable for many application-level identifiers in modern environments.
3. Should I completely stop using Lodash?
No. Lodash still provides many useful utilities. However, if you only need deep cloning or simple object operations, native JavaScript may already cover your use case.
4. Is structuredClone() better than JSON.parse(JSON.stringify())?
Generally yes. It preserves many built-in object types and avoids the data loss that occurs when serializing through JSON.
5. Can Intl.DateTimeFormat replace all date libraries?
No. It excels at formatting, but advanced date calculations, timezone conversions, and recurrence logic often require specialized libraries.
6. Does URLSearchParams work in Node.js?
Yes. Modern Node.js versions support URLSearchParams, making it useful for both browser and server-side code.
7. Should developers avoid npm packages whenever possible?
No. The goal is to avoid unnecessary dependencies, not valuable ones. If a library solves a complex problem well and is actively maintained, it can still be the best choice.
Top comments (0)