The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Many small npm packages exist because the platform once lacked a clean way to do a common task. Several of those gaps have closed. The practical question is no longer whether a native API exists, but whether it behaves the way your code needs, in the browsers and Node.js versions you actually support.
Start with the behavior, not the package
Before deleting a dependency, check four things in order. Most failed replacements break on the second or fourth.
As an Amazon Associate I earn from qualifying purchases.
- Target environments. Write down the minimum browser versions and Node.js version your project supports. A native API that is fine in current Chrome may be missing from your oldest supported target.
- Output semantics. Compare exact output, not just the general idea. A UUID, a digest, a word count and a query string each have precise formats and definitions.
- Error and edge-case behavior. Find out what happens on bad input, cancellation, network errors and HTTP error responses.
- What the package adds. Some packages supply compatibility, algorithms or ergonomics the platform does not. Those are reasons to keep them.
Removing one package also rarely removes all of them. Transitive dependencies and build tooling often remain. To see what is really installed, run npm ls --all before and after the change, and compare the results.
Common replacements, one at a time
Random IDs: crypto.randomUUID() instead of uuid
For a random version 4 UUID, crypto.randomUUID() is available as a global in browsers and on Node.js through the crypto module. In browsers it is limited to secure contexts, meaning HTTPS pages or localhost.
#1 Best Overall
const id = crypto.randomUUID();
// e.g. "3b241101-e2bb-4255-8caf-4136c566a962"
The uuid package still earns its place when you need other UUID versions, such as name-based (v3, v5) or time-based identifiers, or when you rely on its parsing and validation helpers. Do not replace it with Math.random() output. That value is not a UUID, and it is not suitable where unpredictability matters.
Word counts: Intl.Segmenter instead of whitespace splitting
The common text.split(/s+/).length approach is fast but counts punctuation-separated tokens, and it does not handle languages that do not separate words with spaces. Intl.Segmenter with granularity: 'word' lets the runtime’s locale rules decide what a word is.
function countWords(text, locale = 'en') {
const segmenter = new Intl.Segmenter(locale, { granularity: 'word' });
let count = 0;
for (const segment of segmenter.segment(text)) {
if (segment.isWordLike) count++;
}
return count;
}
The counts will differ from whitespace splitting, sometimes in ways that matter for a character or word limit. Decide which definition your product needs, and compare both methods on a sample of your real input before you switch. Pass the locale your content is written in, because segmentation depends on it.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #2
Checksums: crypto.subtle.digest() instead of crypto-js
Web Crypto’s crypto.subtle.digest() computes SHA-family digests asynchronously. It returns an ArrayBuffer, which you convert to hex yourself:
async function sha256Hex(text) {
const bytes = new TextEncoder().encode(text);
const digest = await crypto.subtle.digest('SHA-256', bytes);
return [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
Three limits matter here. First, Web Crypto does not provide MD5, and it is not a synchronous API, so a library that offers MD5 or synchronous hashing still has a use. Second, a plain digest detects accidental corruption. It does not prove authenticity, because anyone can compute the same digest for changed data. Use a keyed construction such as HMAC when tampering matters. Third, passwords need a dedicated password-hashing function, not a bare digest. Node.js documents Web Crypto as a stable API available as globalThis.crypto or require('node:crypto').webcrypto, but it marks some newer algorithm entries as in active development, so check the Node.js documentation for the specific algorithm you use.
HTTP requests and cancellation: fetch with AbortController instead of wrappers
The Fetch API accepts strings, binary data, Blob or File objects, URLSearchParams, FormData and ReadableStream as request bodies, and responses can be read as text, JSON, a Blob or a stream. Many small request wrappers exist only to handle these cases.
Two behaviors need explicit code. Fetch does not reject when the server returns a 404 or 500. The promise resolves with a Response, so check response.ok or response.status yourself. Cancellation works through an AbortController, and the rejection is an AbortError. If the response headers have already arrived, the abort can surface later, during body reading.
async function getJson(url, { timeoutMs = 5000 } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`Request failed with HTTP ${response.status}`);
}
return await response.json();
} finally {
clearTimeout(timer);
}
}
The timer is cleared in finally, so it also covers the body read. This replaces a timeout helper and keeps the error handling visible.
The node-fetch project describes itself as a Fetch-compatible implementation for Node.js and documents where it differs from client-side Fetch. Its v3 line is ESM-only, while v2 supports CommonJS. If your Node.js version already provides the global Fetch behavior your code uses, the package may be redundant. Keep it if you still run CommonJS code on older runtimes or depend on a difference the project documents.
Rank #4
Query strings: URLSearchParams instead of a parsing library
URLSearchParams reads, adds, updates, deletes and iterates query entries, and it can build a URL cleanly:
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', 'native apis');
url.searchParams.append('tag', 'js');
url.searchParams.append('tag', 'npm');
// https://api.example.com/search?q=native+apis&tag=js&tag=npm
Two Node.js details can break an exact-match comparison. Passing an object to the constructor stringifies array values, so { tag: ['js', 'npm'] } becomes a single comma-joined value. To send repeated keys, pass an iterable of key-value pairs. Also, URL serialization and URLSearchParams can percent-encode some characters differently. If you sign or canonicalize URLs, serialize on both sides with the same method and test that path.
URLSearchParams does not handle nested structures such as filter[status]=open. If your API needs that convention, a library or explicit flat keys remain the practical choice.
Best Value
Utility helpers: structuredClone, array methods and where lodash still helps
Several common lodash helpers now have built-in equivalents. structuredClone() covers many deep copies, including Date, Map, Set and typed arrays. It throws on functions, and it does not preserve class prototypes, so it is not a drop-in for _.cloneDeep on arbitrary objects. Array.prototype.flat(), flatMap(), at(), and Object.entries() with Object.fromEntries() cover many simple cases.
Some lodash functions carry behavior that is tedious to reproduce. _.debounce and _.throttle support leading and trailing calls, maxWait, and cancel and flush methods. _.merge and _.isEqual have specific rules for nested objects. If your code uses those options, keep the function or write and test a replacement with the same options.
Replacement map
| Package in use | Native starting point | Keep the package when |
|---|---|---|
| uuid | crypto.randomUUID() |
You need name-based or time-based UUID versions, or its parsing helpers |
| crypto-js | crypto.subtle.digest() |
You need MD5, a synchronous hash, or a legacy algorithm Web Crypto lacks |
| node-fetch | Global fetch |
Your runtime or module system needs its behavior or compatibility |
| Query-string helpers | URLSearchParams |
You need nested objects or a specific array convention |
| lodash (single helpers) | structuredClone(), array methods, Intl.Segmenter |
You rely on options such as debounce’s maxWait or lodash’s merge rules |
Migration checklist
- List every import site of the package, including tests and build scripts.
- Write the native replacement behind the same function signature so callers do not change.
- Compare old and new output on real inputs, including error cases.
- Confirm the replacement works on your minimum supported browsers and Node.js version.
- Remove the package, reinstall, and run
npm ls --allto confirm what changed in the tree.
When to keep the package
- You need an algorithm, UUID version or hash the platform does not provide.
- Your minimum runtime lacks the native behavior and polyfilling costs more than the dependency.
- The package’s edge-case handling is part of what your code relies on, and your tests depend on it.
- The replacement would be a new, untested implementation of security-sensitive logic.
Replacing a small package is worth doing when the native API matches your exact semantics. When it does not, the dependency is doing work, and the honest choice is to keep it.
Recommended Free Tools
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




