This Crazy Syntax Lets You Get An Array Element's Type
Learn how to extract the type of an array element in TypeScript using the powerful Array[number]
trick.
ESBuild is an extremely popular, extremely fast bundler. It's used by frameworks like Vite to bundle frontend code, but it can also be used to bundle Node.js code.
In this article, we'll learn how to use ESBuild to bundle a Node.js app.
We'll be using:
To make our Node app ready for production, we will need a few things:
lint
script to ensure type safety in our code.dev
script to run our code locally and check for TypeScript errors.build
script to bundle our code for production.start
script to run our bundled code in production.package.json
Let's start with an empty repository and initialize it with npm init -y
. This will create a package.json
file.
"type": "module"
in package.json
Next, add "type": "module"
to the package.json
file.
{
// ...other properties
"type": "module"
// ...other properties
}
This tells Node.js to use ES Modules instead of CommonJS modules.
If you don't have pnpm installed, install it.
Next, let's install our dependencies:
pnpm add -D typescript esbuild @types/node npm-run-all
This will add typescript
, esbuild
, @types/node
, and npm-run-all
to our package.json
.
This will also create a pnpm-lock.yaml
. This file keeps track of the exact versions of our dependencies to make installation faster and more predictable.
Add a tsconfig.json
file at the root of the project with the following configuration:
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"verbatimModuleSyntax": true,
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
/* If NOT transpiling with TypeScript: */
"moduleResolution": "Bundler",
"module": "ESNext",
"noEmit": true,
/* If your code doesn't run in the DOM: */
"lib": ["es2022"]
}
}
This configuration is drawn from Total TypeScript's TSConfig Cheat Sheet.
.gitignore
Add a .gitignore
file with the following content:
node_modules
dist
node_modules
contains all of the files we get from npm
. dist
contains all of the files we get from esbuild
.
src
folderCreate a src
folder at the root of the project.
Inside the src
folder, create an index.ts
file with the following content:
console.log("Hello, world!");
lint
scriptAdd a lint
script to package.json
:
{
// ...other properties
"scripts": {
"lint": "tsc"
}
// ...other properties
}
Try running this with pnpm lint
. This will run TypeScript on your code, but it will not output any .js
files.
Here, we are treating TypeScript as a linter to check the correctness of our code.
Try changing console.log
to console.lg
in src/index.ts
. Then run pnpm lint
again - it will report the incorrect code.
build
scriptAdd a build
script to package.json
:
{
// ...other properties
"scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --format=esm"
}
// ...other properties
}
This script bundles our code using esbuild
.
--bundle
indicates that it should output only one file containing our entire bundle.
--platform=node
indicates that it should bundle for Node.js.
--outfile=dist/index.js
indicates that it should output the bundle to dist/index.js
.
--format=esm
indicates that it should use ES Modules for imports and exports.
Try running this with pnpm build
. This will output a dist/index.js
file.
start
scriptAdd a start
script to package.json
:
{
// ...other properties
"scripts": {
"start": "node dist/index.js"
}
// ...other properties
}
This script runs our bundled code using Node.js.
Try running pnpm build && pnpm start
. This will build our code and then run it.
You should see Hello, world!
printed to the console.
dev
scriptThe dev
script will be the most complex. When we run it, we want to do several things at once:
tsc --watch
to check for TypeScript errors.node --watch
to re-run our application when it changes.esbuild --watch
to re-bundle our application when it changes.For each of these, we will add a separate npm
script, then run them all simultaneously using npm-run-all
.
tsc --watch
Add a dev:tsc
script to our package.json
:
{
// ...other properties
"scripts": {
"dev:tsc": "tsc --watch --preserveWatchOutput"
}
// ...other properties
}
The --watch
flag tells TypeScript to re-run when the code changes.
The --preserveWatchOutput
flag tells TypeScript not to clear the console output when it re-runs.
node --watch
Add a dev:node
script to our package.json
:
{
// ...other properties
"scripts": {
"dev:node": "node --watch dist/index.js"
}
// ...other properties
}
esbuild --watch
Add a dev:esbuild
script to our package.json
:
{
// ...other properties
"scripts": {
"dev:esbuild": "pnpm run build --watch"
}
// ...other properties
}
dev
scriptAdd a dev
script to our package.json
:
{
// ...other properties
"scripts": {
"dev": "run-p dev:*"
}
// ...other properties
}
This script runs all the scripts that start with dev:
in parallel.
Try it out by running pnpm dev
. You will see that type checking, bundling, and execution all happen simultaneously.
Congratulations! You now have a fully functional ESBuild/Node setup.
This setup can handle any Node.js code you throw at it, from express
servers to Lambdas.
If you want to see a fully working example, check out this repository.
Build a Node App With TypeScript & ESBuild
Learn how to extract the type of an array element in TypeScript using the powerful Array[number]
trick.
Learn how to publish a package to npm with a complete setup including, TypeScript, Prettier, Vitest, GitHub Actions, and versioning with Changesets.
Enums in TypeScript can be confusing, with differences between numeric and string enums causing unexpected behaviors.
Is TypeScript just a linter? No, but yes.
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
Learn why the order you specify object properties in TypeScript matters and how it can affect type inference in your functions.