Do I still need this dependency for my Node.js app?

A presentation at Minnebar 19 in May 2025 in Minneapolis, MN, USA by Brian Muenzenmeyer

Slide 1

Slide 1

Do I still need this dependency for my Node.js app? Brian Muenzenmeyer Minnebar 19

Slide 2

Slide 2

👋 About Me principal engineer leader of open source program office

Slide 3

Slide 3

approachableopensource.com

Slide 4

Slide 4

web-infra, moderation, triage

Slide 5

Slide 5

📣 Dependencies are great!

Slide 6

Slide 6

🆕 New(ish) Features Feature testing source code watching source code parsing arguments reading environment styling output run scripts run typescript transform typescript Introduced 16.17.0 16.19.0 18.3.0 20.6.0 20.12.0 22.0.0 22.6.0 22.7.0 Release Status Stable as of 20.0.0 Stable as of 20.13.0 Stable as of 20.0.0 Active Development Stable, as of 22.13.0 Stable, as of 22.0.0 Active Development Active Development

Slide 7

Slide 7

Oct 2024 Main Node.js 18 Node.js 20 Jan 2025 Jul 2025 Oct 2025 Jan 2026 Apr 2026 UNSTABLE MAINTENANCE MAINTENANCE Node.js 22 ACTIVE Node.js 23 CURRENT Node.js 24 Apr 2025 MAINTENANCE CURRENT ACTIVE Jul 2026 Oct 2026

Slide 8

Slide 8

Interestingly, these releases potentially replace an external dependency in your project. Feature testing source code watching source code parsing arguments reading environment styling output run typescript Dependency Replaced jest, ava, ts-jest nodemon commander, yargs dotenv colors, chalk ts-node, tsc

Slide 9

Slide 9

Interestingly, these releases potentially replace an external dependency in your project. Feature testing source code watching source code parsing arguments reading environment styling output run typescript Dependency Replaced jest, ava, ts-jest nodemon commander, yargs dotenv colors, chalk ts-node, tsc, deno, bun 🌶️ 🌶️

Slide 10

Slide 10

Slide 11

Slide 11

Slide 12

Slide 12

🥼 Sample Project I’ve prepared a contrived CLI and server as a code sample of incremental migration.

Slide 13

Slide 13

🙉 Timezones… say what?? The business logic isn’t the star here.

Slide 14

Slide 14

🙉 Timezones… say what?? The business logic isn’t the star here. Let’s get started!

Slide 15

Slide 15

Core business logic ripped straight from an LLM cause they are good at generating garbage: export const createUTCDate = ( year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0, ) => { return new Date(Date.UTC(year, month, day, hour, minute, } export const calculateTimeFromNowTo = (dateString) => { const now = new Date() const utcNow = createUTCDate( now getFullYear()

Slide 16

Slide 16

Slide 17

Slide 17

Core business logic ripped straight from an LLM cause they are good at generating garbage: export const createUTCDate = ( year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0, ) => { return new Date(Date.UTC(year, month, day, hour, minute, } export const calculateTimeFromNowTo = (dateString) => { const now = new Date() const utcNow = createUTCDate( now getFullYear() these days we call that vibin’

Slide 18

Slide 18

Slide 19

Slide 19

You saw the CLI earlier. It also has a server.js file copy-pastaed from the Node.js homepage: import { createServer } from ‘node:http’ import { calculateTimeFromNowTo } from ‘./lib/calculate.js’ const server = createServer((req, res) => { res.writeHead(200, { ‘Content-Type’: ‘text/plain’ }) res.end(calculateTimeFromNowTo(‘2028-11-07’))) }) server.listen(3000, ‘127.0.0.1’, () => { console.log(‘Listening on 127.0.0.1:3000’); })

Slide 20

Slide 20

Now, with node src/server.js one can visit http://localhost:3000 and see the same output.

Slide 21

Slide 21

🧪 Testing Source Code Introduced 16.17.0 For many projects, I’d turn to jest to test my code.

Slide 22

Slide 22

🧪 Testing Source Code Introduced 16.17.0 For many projects, I’d turn to jest to test my code. It’s been the default for so long, is part of the OpenJS Foundation, and enjoys a large ecosystem of tools and attention, making it hard to argue against.

Slide 23

Slide 23

We can test our createUTCDate function with this test: import { createUTCDate } from ‘../calculate.js’ describe(‘createUTCDate’, () => { it(‘should create a date in UTC time’, () => { const date = createUTCDate(2026, 3, 20) // zero-inde expect(date.toISOString()).toEqual(‘2026-04-20T00:00 }) })

Slide 24

Slide 24

And then run it with: “test”: “node —experimental-vm-modules node_modules/jest/bi “test”: “pnpm test:jest —watch”,

Slide 25

Slide 25

And then run it with: “test”: “node —experimental-vm-modules node_modules/jest/bi “test”: “pnpm test:jest —watch”, Already there’s trouble brewing…

Slide 26

Slide 26

Node.js now includes a built-in test runner, node —test improving with each successive release. We can replace the jest scripts with: -“test”: “node -“test”: “pnpm +”test”: “node +”test:watch”: —experimental-vm-modules node_modules/jest/b test:jest —watch”, —test”, “node —test —watch”,

Slide 27

Slide 27

Here’s a test diff: +import { describe, it } from ‘node:test’ // no globals import { createUTCDate } from ‘../calculate.js’ describe(‘createUTCDate’, () => { - it(‘should create a date in UTC time’, () => { + it(‘should create a date in UTC time’, (test) => { const date = createUTCDate(2026, 3, 20) expect(date.toISOString()).toEqual(‘2026-04-20T00:00 + test.assert.strictEqual(date.toISOString(),’2026-04}) })

Slide 28

Slide 28

I’m not interested in the code golf here, but it is worth emphasizing two things:

Slide 29

Slide 29

  1. Jest’s support for ESM is still evolving (pinned issue since 2020), not yet with a polished developer experience.

Slide 30

Slide 30

  1. Jest’s support for ESM is still evolving (pinned issue since 2020), not yet with a polished developer experience.

Slide 31

Slide 31

  1. Jest’s support for ESM is still evolving (pinned issue since 2020), not yet with a polished developer experience. 2. Jest is slower, even with one test. Benchmarking via time pnpm test against both showed the Node.js test runner to be 2.2 times faster.

Slide 32

Slide 32

👀 Watching Source Code Introduced 16.19.0 It’s a common convenience to have tasks re-run when as your source code changes during development.

Slide 33

Slide 33

👀 Watching Source Code Introduced 16.19.0 It’s a common convenience to have tasks re-run when as your source code changes during development. Stopping and restarting the server each time is a pain.

Slide 34

Slide 34

Many frameworks and tools have this built in. If not, a common choice is nodemon. Our sample project contains this script in our package.json: “dev”: “nodemon —watch src src/server.js”

Slide 35

Slide 35

Many frameworks and tools have this built in. If not, a common choice is nodemon. Our sample project contains this script in our package.json: “dev”: “nodemon —watch src src/server.js” Works great, and has for a long time.

Slide 36

Slide 36

But now, Node.js has built-in arguments —watch and —watch-path. We can replace this with: -“dev”: “nodemon —watch src src/server.js”, +”dev”: “node —watch-path src src/server.js”,

Slide 37

Slide 37

Not much different at the surface. It works for this use case.

Slide 38

Slide 38

Not much different at the surface. It works for this use case. Critically, it survives parsing errors in the JavaScript.

Slide 39

Slide 39

💬 Parsing Arguments Introduced 18.3.0 Our server and CLI should accept the date to calculate the “time until”.

Slide 40

Slide 40

node src/index.js —to 2027-03-21 # my 40th birthday 💀

Slide 41

Slide 41

Node.js supports process.argv since 0.1.27 so what’s the fuss?

Slide 42

Slide 42

Node.js supports process.argv since 0.1.27 so what’s the fuss? Tools like yargs and commander have been the go-to for a long time.

Slide 43

Slide 43

Our arguments start by being parsed with yargs like this: import yargs from ‘yargs’ import { hideBin } from ‘yargs/helpers’ const values = yargs(hideBin(process.argv)) .option(‘to’, { type: ‘string’, description: ‘Date string to measure time until’ }) .parseSync()

Slide 44

Slide 44

But we can simplify. +import { parseArgs } from ‘node:util’ -import yargs from ‘yargs’ -import { hideBin } from ‘yargs/helpers’ -const values = yargs(hideBin(process.argv)) -.option(‘to’, { type: ‘string’, description: ‘Date string to measure time until’ - }) -.parseSync() +const { values } = parseArgs({ + strict: true, + options: { + to: {

Slide 45

Slide 45

Lops off the two first arguments by default, synchronous by default, and enough for my needs.

Slide 46

Slide 46

Lops off the two first arguments by default, synchronous by default, and enough for my needs. Node.js also throws an error for missing or extra params - which is nice - and again, perhaps enough.

Slide 47

Slide 47

Lops off the two first arguments by default, synchronous by default, and enough for my needs. Node.js also throws an error for missing or extra params - which is nice - and again, perhaps enough. Need something stronger? You already know where to look, or could try a newcomer like bubbletea.

Slide 48

Slide 48

🌲 Reading Environment Introduced 20.6.0 | Active Development Environment variables provide flexibility and portability to code.

Slide 49

Slide 49

The obvious choice for years and years has been dotenv. One would likely reach for this wherever they want to read env: import ‘dotenv/config’ const { PORT } = process.env …

Slide 50

Slide 50

But Node.js can do this too!. Delete that dotenv import.

Slide 51

Slide 51

We add this to our package.json dev script: -“dev”: “node —watch-path src src/server.js”, +”dev”: “node —env-file=.env —watch-path src src/server.js

Slide 52

Slide 52

We get multiple file support with overrides and a familiar enough syntax to dotenv files.

Slide 53

Slide 53

We get multiple file support with overrides and a familiar enough syntax to dotenv files. Node.js can also error or gracefully handle missing env files, the choice is yours.

Slide 54

Slide 54

🖌️ Styling Output Introduced 20.12.0 | Stable as of 22.13.0 Say we got that server.js start message. Maybe we wanna highlight the full URL that some terminals can click on.

Slide 55

Slide 55

The ubiquitous node_module chalk suffices: const { PORT } = process.env import chalk from ‘chalk’ … server.listen(PORT, ‘127.0.0.1’, () => { console.log( Listening on ${chalk.blue(chalk.underline(http://1 ) })

Slide 56

Slide 56

But look at this native Node.js code, quite new if I do say so myself: const { PORT } = process.env -import chalk from ‘chalk’ +import { styleText } from ‘node:util’ server.listen(PORT, ‘127.0.0.1’, () => { console.log( Listening on ${chalk.blue(chalk.underline(http://127.0 + Listening on ${styleText(['underline', 'blue'],http:/ ) })

Slide 57

Slide 57

📜 Run Scripts Introduced 22.0.0 | Stable as of 22.0.0 Agnostic, faster script invocation.

Slide 58

Slide 58

Make your package.json a bit more portable with: - “lint:fix”: “pnpm lint —fix” + “lint:fix”: “node —run lint — —fix”

Slide 59

Slide 59

Executables must be in /node_modules/.bin

Slide 60

Slide 60

Executables must be in /node_modules/.bin In benchmarking, its 200ms faster than npm run

Slide 61

Slide 61

Executables must be in /node_modules/.bin In benchmarking, its 200ms faster than npm run Does not run pre-and post- scripts

Slide 62

Slide 62

🎮 Checkpoint Nice, we got through quite a bit. But it’s always smart to save before the big boss fight.

Slide 63

Slide 63

What’s left?

Slide 64

Slide 64

What’s left? TypeScript.

Slide 65

Slide 65

🏗️ TypeScript Introduced 22.6.0 | Active Development

Slide 66

Slide 66

🏗️ TypeScript Introduced 22.6.0 | Active Development Native TypeScript support has been a consistent community ask, and an obvious wedge issue.

Slide 67

Slide 67

We have a TypeScript error in that server code snippet.

Slide 68

Slide 68

We have a TypeScript error in that server code snippet. Did you see it?

Slide 69

Slide 69

We have a TypeScript error in that server code snippet. Did you see it? How to start?

Slide 70

Slide 70

We have a TypeScript error in that server code snippet. Did you see it? How to start? With a bold rename to server.ts

Slide 71

Slide 71

This is the code in question: const { PORT } = process.env server.listen(PORT, ‘127.0.0.1’, () => { console.log( Listening on ${styleText(['underline', 'blue'],ht ) })

Slide 72

Slide 72

Hint… const { PORT } = process.env // string | undefined, whoops server.listen(PORT, ‘127.0.0.1’, () => { console.log( Listening on ${styleText(['underline', 'blue'],ht ) })

Slide 73

Slide 73

Hint… const { PORT } = process.env // string | undefined, whoops server.listen(PORT, ‘127.0.0.1’, () => { console.log( Listening on ${styleText(['underline', 'blue'],ht ) }) Missing overloads with our inferred typings.

Slide 74

Slide 74

Fixing this is easy enough: -const { PORT } = process.env +const PORT = Number(process.env.PORT)

Slide 75

Slide 75

To run it? We can replace the dev script with: -“dev:node”: “node —env-file=.env —watch-path src src/serv +”dev:node”: “node —experimental-strip-types —env-file=.en

Slide 76

Slide 76

And as of v23.6.0 in January, this runs without the flag at all! node —env-file=.env —watch-path src src/server.ts

Slide 77

Slide 77

🤓 Reply guy: Well actually, you didn’t build the project at all, it only removed the typings, flowstyle. Sad!

Slide 78

Slide 78

🤓 Reply guy: Well actually, you didn’t build the project at all, it only removed the typings, flowstyle. Sad! But wait, I say, my editor gave me immediate feedback of the error, without needing a build process at all.

Slide 79

Slide 79

Some notes: no type-stripping under node_modules

Slide 80

Slide 80

Some notes: no type-stripping under node_modules (yet)

Slide 81

Slide 81

Some notes: no type-stripping under node_modules (yet) —experimental-transform-types

Slide 82

Slide 82

Some notes: no type-stripping under node_modules (yet) —experimental-transform-types —erasableSyntaxOnly for TS@5.8

Slide 83

Slide 83

Slide 84

Slide 84

🛑 STOP Let’s not go deeper today. I’ll spare you the ESM + TypeScript + Jest headache. 👉

Slide 85

Slide 85

Okay, well, can we do the same incremental running of our server.ts file with a dependency? Of course we can. In fact, the ts-node and nodemon docs allude to the fact that this should just work: “dev:nodemon”: “nodemon —watch src src/server.js”,

Slide 86

Slide 86

This was after temporarily dropping the watch glob, as the default is… . . What we uncover, however, is a problem lurking around the whole post, ESM. 🤝

Slide 87

Slide 87

nodemon src/server.ts [nodemon] 3.1.9 [nodemon] to restart at any time, enter rs [nodemon] watching path(s): . [nodemon] watching extensions: ts,json [nodemon] starting ts-node src/server.ts TypeError: Unknown file extension “.ts” for /workspaces/time at Object.getFileProtocolModuleFormat [as file:] (node:i at defaultGetFormat (node:internal/modules/esm/get_forma at defaultLoad (node:internal/modules/esm/load:122:22) at async ModuleLoader.loadAndTranslate (node:internal/mo at async ModuleJob._link (node:internal/modules/esm/modu code: ‘ERR_UNKNOWN_FILE_EXTENSION’ } [nodemon] app crashed - waiting for file changes before star

Slide 88

Slide 88

Ugh. Googling around, this is potentially a “famous” problem with ESM + TypeScript. I won’t even discuss Jest right now. I did get it working but we shouldn’t mention it. 🙊 I’m staying true to this process, so no, we aren’t talking about Bruno and Deno. That’s not the point, yet. We’re almost there, I promise.

Slide 89

Slide 89

tsx I guess is maybe something? This worked: “dev:nodemon”: “nodemon —exec pnpm tsx src/server.ts”

Slide 90

Slide 90

⚖️ Comparisons This ain’t your entire app…

Slide 91

Slide 91

⚖️ Comparisons This ain’t your entire app… …and we can all caveat this with enough asterisks to call in Legal.

Slide 92

Slide 92

⚖️ Comparisons This ain’t your entire app… …and we can all caveat this with enough asterisks to call in Legal. But numbers are numbers.

Slide 93

Slide 93

Metric Before After

node_modules

215 2 49 MB 2.6 MB size node_modules Delta 0.9%, or 107 times smaller 5.3%, or 18 times smaller 🔬 npm-built node_modules, omitting biome dev dependencies

Slide 94

Slide 94

⚡ All this, with two orders of magnitude less dependencies. Dependabot will be bored.

Slide 95

Slide 95

Slide 96

Slide 96

☀️ Pace Layers

Slide 97

Slide 97

symmathesy: together, learn An entity composed by transcontextual mutual learning through interaction — Nora Bateson

Slide 98

Slide 98

Slide 99

Slide 99

🔥 Churn is pace layers in motion.

Slide 100

Slide 100

By design, we can celebrate that:

Slide 101

Slide 101

By design, we can celebrate that: innovation can be quick / creators have agency to explore

Slide 102

Slide 102

By design, we can celebrate that: innovation can be quick / creators have agency to explore competition puts pressure on established systems to improve

Slide 103

Slide 103

By design, we can celebrate that: innovation can be quick / creators have agency to explore competition puts pressure on established systems to improve maintainers craving momentum and stability have space and time to cultivate

Slide 104

Slide 104

By design, we can celebrate that: innovation can be quick / creators have agency to explore competition puts pressure on established systems to improve maintainers craving momentum and stability have space and time to cultivate layers exist for anyone to contribute within their means

Slide 105

Slide 105

“Fast learns; slow remembers.” — Stewart Brand

Slide 106

Slide 106

Find your layer. There’s room for all of us.

Slide 107

Slide 107

Thanks