This article is published in English.
Nine Node.js Utility Packages Worth Adding Before You Start Coding
Learn how a small set of Node.js packages—covering env config, validation, scripting, process handling, and logging—can eliminate common bugs early.
Every new Node project tends to follow the same script: an empty directory, a single index.js, and the naive belief that the standard library will cover most of what you need. A few days in, you're writing your own retry loop by hand, parsing dates with regular expressions, and putting together yet another slightly different .env loader.
At some point it makes sense to stop reinventing these wheels and instead reach for a small, consistent toolkit before writing any real logic. None of these packages are flashy. Each one quietly eliminates a category of bugs that often gets dismissed as an unavoidable cost of building software. Here are nine worth installing early in any project.
1. dotenv
If you've ever pushed an API key to a git repository by mistake, you already understand the appeal of this package. dotenv loads key-value pairs from a .env file into process.env, keeping secrets in a file that stays out of version control instead of embedded directly in your code.
// .env
DATABASE_URL=postgres://localhost:5432/mydb
STRIPE_SECRET_KEY=sk_test_...
// index.js
import "dotenv/config";
const db = connect(process.env.DATABASE_URL);
It's a minimal package with a narrow job, but it makes the difference between having all your configuration in one predictable place versus scattering it across multiple files and a message buried in an old chat thread.
2. zod
Runtime schema validation is easy to dismiss when a handful of if statements seems to cover the same ground. That confidence tends to evaporate the first time a malformed request slips through those checks and reaches production.
zod lets you define a data shape a single time and derive both a runtime validator and a matching TypeScript type from it:
import { z } from "zod";
const CreateUserSchema = z.object({
email: z.string().email(),
age: z.number().min(13),
});type CreateUser = z.infer<typeof CreateUserSchema>;app.post("/users", (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: result.error.flatten() });
}
// result.data is now typed and validated
createUser(result.data);
});
The biggest benefit shows up at your application's boundaries: incoming API payloads, environment variables, configuration files, and anywhere else outside data enters your system. Validating at these entry points means the rest of your code can safely assume the data it receives has the expected shape.
3. tsx
Anyone writing TypeScript has likely run into the overhead of executing a single script: compiling with tsc before running the output, or setting up ts-node and waiting through its startup time. tsx removes that friction entirely by executing .ts files directly, with no build step and no configuration needed.
tsx scripts/migrate-users.ts
It isn't designed to replace a production build pipeline. It's built for the dozens of small scripts every codebase accumulates over time, such as one-off migrations, seed scripts, or quick data checks. These don't warrant a full build setup; they just need to run almost instantly so you don't lose momentum waiting for a compiler.
4. execa
Node's built-in child_process module gets the job done, but using it correctly means manually handling stdout, stderr, exit codes, and errors every time. execa bundles all of that into an interface that works the way you'd naturally expect:
import { execa } from "execa";
const { stdout } = await execa("git", ["rev-parse", "--short", "HEAD"]);
console.log(`Current commit: ${stdout}`);
Errors are thrown as they should be instead of failing silently, output is trimmed for you automatically, and async/await support comes built in without needing to write your own promise wrapper around spawn. For anyone building CLI tools or scripts that call out to other programs, this single package eliminates an entire class of bugs where something quietly does nothing and you're left guessing why.
5. p-retry
Networks are unreliable. APIs enforce rate limits. Database connections sometimes drop for reasons that are never fully explained. p-retry wraps any async function with retry logic and exponential backoff, so one flaky call doesn't bring down your entire process:
import pRetry from "p-retry";
const data = await pRetry(() => fetchFromFlakyApi(url), {
retries: 5,
onFailedAttempt: (error) => {
console.log(`Attempt ${error.attemptNumber} failed. Retrying...`);
},
});
It's common to end up writing this kind of logic manually for every project, usually with mistakes, and often without proper backoff, which can end up hammering a struggling API even harder with rapid retries. This package handles it correctly in only a handful of lines, and it has kept more than one integration from failing outright during a routine outage on a third-party service.
6. day.js
Working with dates in plain JavaScript is notoriously awkward, and moment.js, the library most people used to default to, is heavy and no longer under active development. day.js offers a similarly convenient API but at a much smaller footprint:
import dayjs from "dayjs";
const deadline = dayjs().add(3, "day").format("YYYY-MM-DD");
const isOverdue = dayjs(invoice.dueDate).isBefore(dayjs());
Formatting dates, comparing them, adding or subtracting time spans, and parsing messy date strings all become straightforward, which means you stop introducing subtle off-by-one-day bugs that come from doing this arithmetic manually.
7. pino
console.log works fine for small scripts, but once you're running a production service producing thousands of log entries per minute, you need something you can actually filter and search. pino outputs structured JSON logs at a speed that has almost no measurable impact on your application:
import pino from "pino";
const logger = pino();
logger.info({ userId: user.id, action: "checkout" }, "Order placed");
Because the output is structured, whatever tool you use to consume it, Datadog, an ELK stack, or even just grep-ing through a file later, can parse and filter it properly. That beats scrolling through pages of plain text trying to spot the one relevant line while something is on fire.
8. cheerio
Sometimes all you need is to extract a piece of structured data from an HTML page, and spinning up a full headless browser feels like massive overkill for a task that's really just "locate this element and read its text." cheerio provides a jQuery-like API for parsing and querying HTML server-side, without any of the cost of launching an actual browser:
import * as cheerio from "cheerio";
const $ = cheerio.load(html);
const titles = $("h2.product-title")
.map((_, el) => $(el).text().trim())
.get();
It doesn't execute JavaScript on the page, so it can't substitute for a tool like Playwright when content is rendered client-side. But for scraping static markup, working through feed-like content, or extracting values from pages you control, it's much faster and lighter than booting up a browser instance.
9. pm2
Starting your app with node index.js is fine right up until it crashes in the middle of the night and nothing restarts it. pm2 keeps your process running, automatically restarts it after a crash, and provides basic monitoring and log handling, all without requiring a full container orchestration platform.
pm2 start index.js --name my-api
pm2 logs my-api
pm2 restart my-api
For many small and mid-sized deployments, this is really all the process supervision you'll ever need. It won't take the place of Kubernetes if you're managing a large distributed system, but on a single server running a couple of Node processes, it's the difference between having to SSH in every time something dies and simply not having that problem to begin with.
The Actual Point
None of these nine packages is particularly flashy on its own, and that's actually the takeaway. Each one takes over some small chunk of logic you would otherwise end up writing yourself, usually getting it slightly wrong the first pass, and then having to maintain indefinitely. Reaching for them isn't about cutting corners. It's about directing your limited focus toward the parts of the application that are genuinely yours to build, rather than re-implementing and re-debugging a retry loop for the fifth project running.