skip to content

The Dismal *Amazing* Thoughts and Adventures of

James Gardner

A Modern Node Application

Getting Modern with Node 20 and Tooling

/ 3 min read

The release of Node.js 20 brings with it a suite of notable features, significantly enriching the Node.js landscape. This version marks a step forward in Node.js’s evolution, offering developers new tools and capabilities that promise to reshape how we manage and develop our projects.

TSX: Moving Beyond Nodemon

The combination of Nodemon and ts-node has long been a cornerstone in my development workflow. The seamless integration of ts-node with nodemon was a welcome improvement, simplifying our setups. However, the emergence of TSX represents an even more significant leap forward. TSX effectively replaces the functions of nodemon and ts-node into a single, efficient tool, enhancing the development experience.

Node 20.6.x Makes dotenv Redundant

Dotenv has been a mainstay in my Node.js projects for its simple yet effective management of environment variables. With the advancements in Node 20.6.x, however, I don’t need it anymore as we have built in .env support:

tsx watch --env-file=.env src/index.ts"

This aligns perfectly with convict.js configurations, allowing for seamless overrides of default settings with environment variables:


const config = convict({
  ip: {
    doc: 'The IP address to bind.',
    format: 'ipaddress',
    default: '',
    env: 'IP_ADDRESS',
  port: {
    doc: 'The port to bind.',
    format: 'port',
    default: 3000,
    env: 'PORT',

Remember, TSX is a wrapper around node which means you can pass the same node options to it.

Built in Test Runner

Setting up Jest with TypeScript has traditionally involved navigating a maze of choices between ts-jest, babel, and various configurations. Node.js addresses this complexity with its own built-in test runner. While it might not offer the full suite of features of Mocha or Jest, it meets most of my testing needs. The ease of running TypeScript tests with TSX is a testament to this:

node --import tsx --test test/**/*.spec.ts

import assert from 'node:assert';
import { describe, it, before} from 'node:test';
import fastify, { FastifyInstance } from 'fastify';
import customers from '../../../src/app/customers';

describe('customers', () => {
  let app: FastifyInstance;

  before(() => {
    app = fastify();

  it('returns 200 with customers filtered by unavailable status', async () => {
    const response = await app.inject({
      method: 'get',
      url: '/customers',
      query: {
        status: 'unverified'

    assert.equal(response.statusCode, 200);


Regularly revisiting the tools and runtime versions in our projects is crucial. The advancements in Node.js, particularly version 20, remind us of the ever-evolving nature of our field. Adopting these updates is not merely about keeping up-to-date; it’s about enhancing our development processes for greater efficiency and performance, ensuring that our practices remain robust and adaptable for the future.