Let’s try to set up a Node.js/Express.js TypeScript project with nodemon and ESM!
Yesterday someone in the ZTM Discord server asked if it was possible to use nodemon
with TypeScript and native ECMAScript modules.
It is!
I used Node.js (version 14 works) and a bit of internet sleuthing to figure out how to do it.
TypeScript
Create a new directory. Inside that directory, we’ll need to initialize a new Node.js project:
npm init -y
Now for the dependencies. First, Express.js:
npm i express
As development dependencies, we use TypeScript, nodemon, ts-node and the necessary types:
npm i --save-dev typescript nodemon ts-node @types/node @types/express
Now, TypeScript setup:
npx tsc --init
The above command creates a new file called tsconfig.json
. Adjust the following parts in the file:
{
"module": "ES2020",
"moduleResolution": "Node",
"outDir": "./dist"
}
Compiled files (from TypeScript to JavaScript) will land in the dist folder.
Add these lines to package.json
to enable ECMAScript modules and allow imports from your compiled TypeScript files:
{
"type": "module",
"exports": "./dist/index.js"
}
Minimal Server
Let’s create our source code. Make a new folder called src
and add a file called index.ts
inside that directory.
Here’s a minimal Express server:
import express, { Request, Response } from 'express'
const app = express()
const port = 5000
app.get('/', (req: Request, res: Response) => {
res.json({ greeting: 'Hello world!' })
})
app.listen(port, () => {
console.log(`🚀 server started at http://localhost:${port}`)
})
Wiring Up Scripts
Now, the magic will come together. Add the following script to package.json
:
{
"scripts": {
"dev:server": "nodemon --watch './**/*.ts' --exec 'node --experimental-specifier-resolution=node --loader ts-node/esm' src/index.ts"
}
}
First, we’ll use nodemon
with the --watch
flag to keep track of all TypeScript files. We can use --execute
to run other scripts.
We use the experimental loader feature with hooks to run ts-node
. We need the library so that we can directly run TypeScript on Node.js:
It JIT transforms TypeScript into JavaScript, enabling you to directly execute TypeScript on Node.js without precompiling. This is accomplished by hooking node’s module loading APIs, enabling it to be used seamlessly alongside other Node.js tools and libraries.
Start the server now:
npm run dev:server
Yay, it works!
Importing Files
You probably want to split up your code into different files and import them.
You cannot import a TypeScript file directly.
That means that you first have to transpile all TypeScript files it into JavaScript and then import the JavaScript files.
Using the node --experimental-specifier-resolution=node
in the start command is a first step. Enabling the flag allows you to use the standard import syntax without using a file ending. This works as known:
import { blababla } from './some-folder/some-file'
I will use tsc-watch to run tsc
in watch mode and delegate to nodemon
if the compilation is successful.
npm install --save-dev tsc-watch
Adjust package.json
:
{
"scripts": {
"watch": "nodemon --watch './**/*.{ts,graphql}' --exec 'node --experimental-specifier-resolution=node --loader ts-node/esm' src/index.ts",
"dev": "tsc-watch --onSuccess \"npm run watch\""
}
}
tsc
will write the JavaScript files into the specified outDir
location (see tsconfig.json
). We’ve set the folder to ./dist
.
In package.json
we added an exports
key-value-pair which allows us to import those transpiled files from the dist
folder as if they were the original TypeScript files.
Let’s say that you have a folder structure like this:
.
├── dist
│ ├── index.js
│ ├── services
│ │ └── accounts
│ │ ├── index.js
│ │ ├── resolvers.js
│ │ └── typeDefs.js
│ └── utils
│ └── apollo.js
├── node_modules
├── package.json
├── package-lock.json
├── src
│ ├── index.ts
│ ├── services
│ │ └── accounts
│ │ ├── index.ts
│ │ ├── resolvers.ts
│ │ └── typeDefs.ts
│ └── utils
│ └── apollo.ts
└── tsconfig.json
In src/index.ts
you want to import something from src/services/accounts/index.ts
. It works like normal JavaScript even though the files are TypeScript files:
// src/index.ts
import { startApolloServer } from './services/accounts/index'
Node.js will use your configuration to import the according JavaScript file under the hood.
Thoughts
It was a bit tricky to find out how to pair nodemon
with the Node.js loader feature. While you’ll get console warnings about using this experimental feature, it works fine on the latest Node.js v14.
Success.