1Node.js Is Finally a "Complete" Runtime
For years, Node.js developers lived in npm-dependency hell. Need to read environment variables? Install dotenv. Need to run tests? Install jest or mocha. Need to watch files? Install nodemon. Those days are fading fast.
With Node.js 22+ (the current LTS as of 2026), you get a lot of these things built-in, out of the box:
- ✅ Native test runner —
node:testmodule, no jest needed for basic testing - ✅ Native
.envloading —node --env-file=.env app.js - ✅ Native file watching —
node --watch app.jsreplaces nodemon - ✅ Native TypeScript support (type stripping) — run
.tsfiles directly - ✅ Native WebSocket client — no
wspackage needed in many cases
node_modules, and faster CI pipelines. If you're still reaching for old dependencies out of habit, it's time to check if Node has you covered natively.
2TypeScript Without a Build StepNode 22+
This is arguably the most exciting recent shift. Node.js 22 introduced type stripping — you can now run TypeScript files directly without tsc, ts-node, or esbuild in your development workflow.
// user.ts — no compilation step needed in devtype User = { id: number; name: string; email: string; }; function greetUser(user: User): string { return `Hello, ${user.name}! Welcome back.`; } const user: User = { id: 1, name: "Ravi", email: "ravi@example.com" }; console.log(greetUser(user));
# Just run it directly — no tsc, no ts-node!
node user.tsType stripping removes type annotations at runtime but does NOT do type checking. You still want tsc --noEmit in your CI pipeline to catch type errors before production.
For production, you still transpile. But for local dev and scripting? This is a game changer. Your team's onboarding just got 30 minutes shorter.
3Async Patterns That Actually Work at Scale
AsyncLocalStorage from node:async_hooks lets you pass request context (like a user ID or trace ID) across your entire call stack without prop-drilling it through every function:import { AsyncLocalStorage } from 'node:async_hooks';
const requestContext = new AsyncLocalStorage<{ userId: string; traceId: string }>();
// Middleware sets context once
function requestMiddleware(req, res, next) {
const store = { userId: req.user.id, traceId: req.headers['x-trace-id'] };
requestContext.run(store, next);
}
// Deep inside any service — access it without passing it as a param
function logDatabaseQuery(query: string) {
const ctx = requestContext.getStore();
console.log(`[User: ${ctx?.userId}] Query: ${query}`);
}
This is how enterprise-grade Node apps do distributed tracing without a heavy framework. Clean, built-in, and zero overhead from external libraries.
4The Module System Is Settled — Use ESM
Okay, I'll say it plainly: the CommonJS vs ESM debate is over. ESM (ES Modules) is the way forward. If you're still writing require() in new projects, you're building on a foundation that Node.js is actively moving away from.
// ❌ Old way — CommonJSconst express = require('express'); const { readFile } = require('fs'); // ✅ Modern way — ESM import express from 'express'; import { readFile } from 'node:fs/promises';
To enable ESM in your project, set "type": "module" in your package.json — that's it. One line. You can also use the .mjs extension per file if you're migrating gradually.
Some older npm packages are CommonJS-only. You can still import them in ESM, but you can't require() an ESM package in CommonJS. Always check the package's docs when migrating.
5Built-in Test Runner — Stop Installing Jest for Everything
The node:test module has matured beautifully. For unit tests and integration tests that don't need a fancy UI, it does the job cleanly:
import assert from 'node:assert/strict';
import test from 'node:test';
// Simple unit test — no setup, no config files
test('adds two numbers correctly', () => {
assert.equal(2 + 2, 4);
});
test('async fetch returns data', async () => {
const data = await fetchUserById(1);
assert.ok(data.name);
assert.equal(data.id, 1);
});
# Run all test files
node --test
# Run with code coverage
node --test --experimental-test-coverage
For large-scale projects with mocking, snapshot testing, and parallel execution — Jest or Vitest still make sense. But for small services, scripts, and utilities? node:test is all you need.
6Practical Project Structure for 2026
After working on enough Node projects, here's a structure that scales without becoming a maze:
my-api/├── src/ │ ├── controllers/ # Route handlers, thin layer │ ├── services/ # Business logic lives here │ ├── repositories/ # DB queries, data access │ ├── middlewares/ # Auth, logging, validation │ ├── utils/ # Pure helper functions │ └── app.ts # Express/Fastify setup ├── tests/ ├── .env.example ├── package.json └── tsconfig.json
The key principle: your services should have zero knowledge of HTTP. They take plain inputs, return plain outputs. Controllers handle the HTTP layer. This makes your business logic testable without spinning up a server — and trust me, you'll thank yourself for this separation at 2am during a production incident.
7Performance Tip: Use Worker Threads for CPU Work
Node.js is single-threaded for JavaScript execution. This is great for I/O but terrible for CPU-heavy tasks like image processing, PDF generation, or parsing large JSON files. The fix? Worker Threads.
// worker.js — runs in a separate thread
import { workerData, parentPort } from 'node:worker_threads';
function heavyCalculation(data) {
// Simulate CPU-intensive work
return data.reduce((sum, n) => sum + n * n, 0);
}
parentPort.postMessage(heavyCalculation(workerData));
// main.js — your main thread stays free
import { Worker } from 'node:worker_threads';
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
});
}
const result = await runWorker([1, 2, 3, 4, 5]);
console.log('Result:', result); // 55
Your main event loop stays free for handling incoming requests while the CPU work happens in parallel. This alone can prevent timeouts and latency spikes under load.
8Quick Wins Checklist for Your Next Node.js Project
- ๐ Use
helmetin Express — sets secure HTTP headers in one line - ๐ฆ Use
zodfor runtime input validation — type-safe and composable - ๐ Use
--watchflag instead of nodemon in dev - ๐งน Always handle
unhandledRejectionanduncaughtExceptionglobally - ๐ Use
pinofor logging — it's 5x faster thanwinstonwith JSON output - ๐ฆ Use rate limiting with
express-rate-limiton all public endpoints - ๐️ Structure your
.envfiles with an.env.examplecommitted to Git - ⚡ Try Fastify if performance is critical — it's 2-3x faster than Express on benchmarks
๐ What's Your Node.js Setup?
Drop a comment below — are you still on CommonJS or have you fully moved to ESM? Using Fastify or sticking with Express? Would love to see what setups others are running in 2026. Happy coding! ๐ง๐ป
