Node.js grew up — you might not need all those packages

Date: 9 April 2026
Author: Miro Ostafinski
Tags: nodejs, javascript

The old ritual

For years, starting a Node.js project began with the same shopping list. You'd install axios to make web requests, jest to run tests, nodemon to restart the app when you saved a file, dotenv to read configuration, and ts-node if you wanted TypeScript. None of it was your app yet — it was just the plumbing everybody needed.

Quietly, over the last few releases, Node.js absorbed almost all of it. If you learned Node a few years ago and haven't checked what's built in lately, this post is your catch-up. Everything below works in Node 22 and newer with no packages installed.

Making web requests: fetch is just there

The fetch function — the same one browsers have — is built into Node now. No import, no install:

const response = await fetch('https://api.github.com/users/octocat');
const user = await response.json();
console.log(user.name);

That covers the vast majority of what people installed axios or node-fetch for. If you're writing a new project and your first instinct is npm install axios — pause. Plain fetch probably does the job.

Running tests: a test runner ships in the box

Node has a built-in test runner. You import it from node:test, write tests in a familiar style, and run them with node --test:

import { test } from 'node:test';
import assert from 'node:assert';

test('adds two numbers', () => {
  assert.strictEqual(2 + 2, 4);
});
node --test

It finds your test files, runs them, and prints a readable report. It handles the essentials — grouping tests, skipping, mocking, even code coverage with --experimental-test-coverage. For a library or a small service, it's genuinely enough. Jest and Vitest still earn their place in bigger projects (better watch mode, richer ecosystem), but "I need to install a test framework before I can write a single test" is no longer true.

Restarting on file changes: --watch

nodemon had one job: restart your app when you save a file. Node does that itself now:

node --watch server.js

Save a file, the process restarts. That's it. One less tool to configure.

Reading .env files: --env-file

Almost every project keeps secrets and settings in a .env file, and almost every project installed dotenv to read it. Now:

node --env-file=.env server.js

Your process.env values are populated, exactly as with dotenv. There's also --env-file-if-exists for when the file is optional — handy so the same command works both locally and in production, where settings usually come from the hosting platform instead of a file.

TypeScript: Node runs it directly

This is the big one. Modern Node can run TypeScript files without any compilation step:

node app.ts

The honest fine print: Node doesn't check your types — it strips them out and runs the JavaScript underneath. Type checking is still your editor's job (or tsc --noEmit in CI). And a few TypeScript features that generate extra code, like enum, need a flag. But for the everyday case — you write TypeScript, you want to run the file — it just works. The ts-node era is over.

The sleeper hit: a built-in database

Node now includes SQLite, a small file-based database, in the standard library:

import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync('app.db');
db.exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT)');

db.prepare('INSERT INTO notes (text) VALUES (?)').run('hello');
const notes = db.prepare('SELECT * FROM notes').all();

For scripts, prototypes and small tools, this removes the "but I'd have to set up a database" excuse entirely. Your data lives in a single file next to your code.

What this changes in practice

Fewer packages isn't just tidiness. Every dependency is code you didn't write but are responsible for — it can break on update, conflict with another package, or be abandoned. The built-in versions are maintained with Node itself and follow web standards where they exist (like fetch), so what you learn transfers to the browser too.

My rule for new projects now: start with zero dependencies and add packages only when you hit a real limit. You'll hit limits — a complex app still wants a proper framework, a big test suite still wants Vitest. But you'll be surprised how far the standard library takes you, and how pleasant package.json looks with three entries instead of thirty.