Node.js Tips That Actually Matter in 2026

The tips that matter in 2026 are the ones that delete dependencies. Use node --watch instead of nodemon, run TypeScript files directly with Node 24's native type stripping, and test with the built-in node:test runner. Fewer moving parts, less config, faster startup.
Part of the complete web development guide.
On this page
- Stop reaching for nodemon and ts-node
- Use the built-in test runner
- Learn where your event loop actually blocks
- Handle errors like the process is going to die
- Read your dependencies before you install them
- Pick ESM and stop fighting it
- A word on structure
- FAQ
- Should I use Node.js or Bun in 2026?
- Do I still need a framework like Express?
- What Node version should I be on?
Most Node.js tip lists are recycled from 2019. The runtime has changed a lot since then, and a few of those old habits now actively hurt you. Here’s what I’d tell a developer starting a Node project in 2026, based on what I actually run and debug.

Stop reaching for nodemon and ts-node
Node has had a built-in watch mode since v18.11, and --watch is stable in current releases. You don’t need a dev dependency for the most common thing you do all day.
node --watch server.js
# or with TypeScript type stripping (Node 22.18+ / 23.6+, on by default in 24)
node --watch server.ts
Node 24 strips TypeScript types natively. That means you can run a .ts file without a build step or a loader, as long as you stick to erasable syntax (no enums, no namespaces, no decorators that emit code). If your project leans on those features, keep your build pipeline. If it doesn’t, deleting ts-node and a tsconfig build step is a real simplification, not a novelty.

Use the built-in test runner
node:test is stable and good enough that pulling in Jest or Mocha for a new project is hard to justify. It runs fast, has no config, and the assertion module covers most of what you need.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
describe('parseOrder', () => {
test('rejects a missing id', () => {
assert.throws(() => parseOrder({ total: 10 }), /id is required/);
});
test('coerces total to cents', () => {
assert.equal(parseOrder({ id: 'a1', total: 10.5 }).cents, 1050);
});
});
node --test
node --test --experimental-test-coverage
The one thing people miss from Jest is mocking. Node added mock helpers in node:test, so you can spy on methods and fake timers without a library. It’s less polished than Jest’s API, but you stop paying the startup cost and the dependency churn.
Learn where your event loop actually blocks
This is the mistake I see most in code reviews. Node is single-threaded for JavaScript, so one synchronous CPU-heavy call stalls every request behind it. JSON.parse on a 40MB payload, a regex with catastrophic backtracking, a synchronous fs call in a request handler. None of these throw. They just make your app feel slow under load.
// Don't do this in a request handler
const data = JSON.parse(await fs.readFile('./huge.json', 'utf8'));
// Stream it, or move it off the main thread
import { Worker } from 'node:worker_threads';
function parseInWorker(file) {
return new Promise((resolve, reject) => {
const w = new Worker(new URL('./parse-worker.js', import.meta.url));
w.once('message', resolve);
w.once('error', reject);
});
}
Worker threads aren’t free. Spawning one per request is worse than the blocking you were trying to avoid. Use a pool, or push the work to a queue and process it out of band. And measure before you optimize: node --prof and --cpu-prof will tell you where the time actually goes. Guessing is how you end up adding complexity that fixes nothing.
Handle errors like the process is going to die
An unhandled rejection in Node crashes the process by default now, and that’s the right behavior. What’s wrong is letting it happen silently in production.
process.on('unhandledRejection', (reason) => {
logger.fatal({ reason }, 'unhandled rejection');
process.exit(1);
});
process.on('uncaughtException', (err) => {
logger.fatal({ err }, 'uncaught exception');
process.exit(1);
});
Log it, exit, let your process manager restart it. Don’t try to keep going after an uncaught exception. The process is in an unknown state and the next request might corrupt data. If you’re running on something like PM2 or a container orchestrator, a clean exit and restart is the safest recovery you have.
Read your dependencies before you install them
The npm supply chain is still the softest target in most stacks. A package with 4 million weekly downloads got compromised last year and pushed malicious code for hours before anyone caught it. A few habits that cost you almost nothing:
- Pin exact versions and commit your lockfile. Never run
npm installin CI withoutnpm ci. - Run
npm auditand actually read the output, not just the summary line. - Turn on
ignore-scriptsfor installs you haven’t vetted. Postinstall scripts are how a lot of these attacks run. - Check the publish date and maintainer history on anything new before it goes in your
package.json.
None of this is exciting, and that’s the point. The boring hygiene is what keeps you out of an incident writeup.
Pick ESM and stop fighting it
If you’re starting fresh in 2026, write ESM. The ecosystem has caught up, and the interop headaches that made people stay on CommonJS are mostly gone. A couple of things that trip people up:
// package.json
{ "type": "module" }
// __dirname doesn't exist in ESM
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// importing a CJS package still works, but named exports may not
import pkg from 'some-cjs-package';
const { thing } = pkg;
Top-level await works in ESM, which cleans up a lot of init code. Just remember it runs before your module finishes loading, so don’t put a network call there that a hundred other modules depend on.
A word on structure
Most Node codebases I get called in to fix don’t have a performance problem. They have a structure problem: business logic tangled into route handlers, no clear boundary between the HTTP layer and the domain, and tests that need a running server to pass. Fixing that is unglamorous work, but it’s what makes the tips above easy to apply instead of a fight. If you’re building or rebuilding something on the web and want a second set of eyes on the architecture, this is the kind of thing I do through custom web development for clients around Long Island and beyond.
FAQ
Should I use Node.js or Bun in 2026?
Node is the safer default. It’s stable, every hosting provider supports it, and the features that made Bun appealing (fast startup, built-in TypeScript, a test runner) now exist in Node. Bun is worth trying for scripts and side projects, but I wouldn’t move a production app onto it just for speed unless you’ve benchmarked your actual workload and found Node is the bottleneck.
Do I still need a framework like Express?
For anything with real routing, middleware, and error handling, yes, use something. Express is fine and still maintained, but Fastify is faster and has better built-in validation and schema support. For small services, Node’s built-in http module plus a router is often enough.
What Node version should I be on?
Run the current LTS release, which is Node 24 as of early 2026. Even-numbered releases get long-term support; odd-numbered ones are for testing what’s coming. Don’t run an odd release in production.


