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 ts-watch to run tsc in watch mode and delegate to nodemon if the compilation is successful.

npm install --save-dev ts-watch

Adjust package.json:

{
  "scripts": {
    "watch": "nodemon --watch './**/*.{ts,graphql}' -r dotenv/config --exec 'node --experimental-specifier-resolution=node --loader ts-node/esm' src/index.ts",
    "dev": "npx 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.

Resources