Angular Icon Get 67% off the Angular Master Bundle!

See the bundle then add to cart and your discount is applied.

0 days
00 hours
00 mins
00 secs
Node

The Ultimate Guide to setting up Node.js with TypeScript and Express

There are so many tutorials online about how to setup TypeScript with Node.js and Express, but I found them all overly confusing, incomplete and missing important features such as recompilation, reloading and final build steps.

Not only this, they miss out vital explanations and gloss over details. This post is aimed to be a comprehensive “how-to” guide on setting up your own Node.js server written in TypeScript.

✅ tl:dr; Check the Node + TypeScript + Express project on GitHub then follow along below!

We’ll be using Express to then send back some data which will be more than enough to get you started. Ready? Let’s dive in!

Project setup

First, we’ll need to setup our workspace, or project. I’ll be using VSCode, as it’s absolutely fantastic. You’re welcome to use whatever you want.

Open up a new Terminal window in VSCode or iTerm/etc.

When you open up your Terminal, use the cd <directory-name> to move into the directory you wish to create the project inside.

For me, that’s this command:

cd Documents\GitHub

Now, we need to create the project folder:

mkdir node-express-typescript

Then cd into that project folder:

cd node-express-typescript

Now we’re ready to get setup!

Initialize a new GitHub project

We’ll be using GitHub, if you don’t wish to push the code to your GitHub account then skip this step.

Go to GitHub and create a new repo. Mine will be called node-express-typescript and therefore located at github.com/ultimatecourses/node-express-typescript.

We’re not going to clone the repo, we’ll connect it later after adding our files. Now time to create the package.json which will specify our project dependencies, and hold our npm scripts that we’ll run our TypeScript project with!

Creating a package.json

Before we can get our Node, Express and TypeScript project up and running, we need a package.json to then declare and enable us to install the project dependencies.

Run the following to initialize a new project:

npm init

This will then walk you through a few steps, as we’ve already initialized a GitHub repository, we can use the URL during the installation to include it in our package.json for us.

I’ve detailed each step below in a # comment so please copy them accurately (using your own username/repo name):

# node-express-typescript
package name: (express-typescript)

# Just press "Enter" as we don't need to worry about this option
version: (1.0.0)

# Node.js setup with Express and TypeScript
description:

# dist/index.js
entry point: (index.js)

# Just press "Enter"
test command:

# Enter the GitHub URL you created earlier
git repository: (https://github.com/ultimatecourses/node-express-typescript)

# Just press "Enter"
keywords:

# Add your name
author: ultimatecourses

# I like to specify the MIT license
license: (ISC) MIT

Once you’ve reached the last step it will say:

Is this OK? (yes)

Hit enter and you’ll see your new package.json file that should look something like this:

{
  "name": "node-express-typescript",
  "version": "1.0.0",
  "description": "Node.js setup with Express and TypeScript",
  "main": "dist/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ultimatecourses/node-express-typescript.git"
  },
  "author": "ultimatecourses",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/ultimatecourses/node-express-typescript/issues"
  },
  "homepage": "https://github.com/ultimatecourses/node-express-typescript#readme"
}

Installing TypeScript

Next up, run the following to install TypeScript as a local project dependency:

npm i typescript

With TypeScript installed locally, not globally, you should see this added to your package.json:

{
  //...
  "dependencies": {
    "typescript": "^4.1.3"
  }
}

✨ You’ll also have a new package-lock.json file generated, which we’ll want to commit to Git shortly. You don’t need to do anything with this file.

Generating a tsconfig.json

To configure a TypeScript project, it’s best to create a tsconfig.json to provide some sensible defaults and tweaks to tell the TypeScript compiler what to do. Otherwise, we can pass in compiler options.

Typically, you would npm i -g typescript (the -g means global) which then allows us to run tsc to create a tsconfig.json. However, with npm we have something called npx which the “x” essentially means “execute”.

This allows us to skip a global install and use tsc within the local project to create a tsconfig.json.

If we did try to run tsc --init to create a tsconfig.json, we’d see this error (because typescript would not be available globally, thus tsc also would be unavailable):

⛔ tsc : The term 'tsc' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of 
the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ tsc --init
+ ~~~
    + CategoryInfo          : ObjectNotFound: (tsc:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

This is where npx comes into play to execute the local version of our typescript install, let’s run the following:

npx tsc --init

And you should see something like this:

message TS6071: Successfully created a tsconfig.json file.

Now checkout your tsconfig.json file. Yep, it looks a little overwhelming as there are lots of comments in there, but don’t worry about them too much, there are some great sensible defaults that you can simply leave as they are.

However, we’re going to make a quick tweak. Find the outDir line:

// "outDir": "./",

Uncomment it and change it to:

"outDir": "dist",

🚀 This dist folder will contain our compiled code, just JavaScript, which will then be served up by Node.js when we run our app. We write our code with TypeScript, recompile and serve the dist directory as the final output. That’s it!

If you clean up all the comments and remove unused options, you’ll end up with a tsconfig.json like this:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Project Dependencies

As we’re going to create a Node.js application with Express, we’ll need to install a few bits and pieces.

Production Dependencies

Here’s what we can run next:

npm i body-parser cross-env dotenv express helmet rimraf

Here are some further details on each of those:

  • body-parser extracts the entire body of an incoming request stream (for Express) and exposes it on req.body as something easier to work with, typically using JSON.
  • cross-env sets environment variables without us having to worry about the platform.
  • dot-env loads in .env variables into process.env so we can access them inside our *.ts files.
  • express is a framework for building APIs, such as handling GET, POST, PUT, DELETE requests with ease and building your application around it. It’s simple and extremely commonly used.
  • helmet adds some sensible default security Headers to your app.
  • rimraf is essentially a cross-platform rm -rf for Node.js so we can delete older copies of our dist directory before recompiling a new dist

Most of these packages ship with type definitions for TypeScript, so we can start using them right away. For anything else, their type definitions can usually be found on Definitely Typed.

Let’s install the types for the packages that don’t ship with them be default:

npm i @types/body-parser @types/express @types/node

You’ll notice I’ve thrown in @types/node, which are type definitions for Node.js.

JavaScript Array Methods eBook Cover

🎉 Download it free!

Ready to go beyond ForEach? Get confident with advanced methods - Reduce, Find, Filter, Every, Some and Map.

  • Green Tick Icon Fully understand how to manage JavaScript Data Structures with immutable operations
  • Green Tick Icon 31 pages of deep-dive syntax, real-world examples, tips and tricks
  • Green Tick Icon Write cleaner and better-structured programming logic within 3 hours

As an extra bonus, we'll also send you some extra goodies across a few extra emails.

Development Dependencies

As we need to develop our Node.js and TypeScript app locally, we’ll want to use nodemon to monitor changes to our files. Similarly, as we want to watch our TypeScript code for changes, we’ll install concurrently. This allows us to run multiple commands at the same time (tsc --watch and nodemon).

Don’t worry, this is the final install, which using --save-dev will save to the devDependencies prop inside our package.json (instead of just dependencies like a regular npm i <package>):

npm i --save-dev concurrently nodemon

We should then end up with a nice package.json that looks like so:

{
  "name": "node-express-typescript",
  "version": "1.0.0",
  "description": "Node.js setup with Express and TypeScript",
  "main": "dist/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ultimatecourses/node-express-typescript.git"
  },
  "author": "ultimatecourses",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/ultimatecourses/node-express-typescript/issues"
  },
  "homepage": "https://github.com/ultimatecourses/node-express-typescript#readme",
  "dependencies": {
    "@types/body-parser": "^1.19.0",
    "@types/express": "^4.17.11",
    "@types/node": "^14.14.22",
    "body-parser": "^1.19.0",
    "cross-env": "^7.0.3",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "helmet": "^4.4.1",
    "rimraf": "^3.0.2",
    "typescript": "^4.1.3"
  },
  "devDependencies": {
    "concurrently": "^5.3.0",
    "nodemon": "^2.0.7"
  }
}

You’ll notice your package-lock.json file will also be updated too, we’ll commit this to GitHub shortly too.

NPM Scripts: Serve and Start

Now time to get our project running locally via localhost, then we’ll talk about how it will “run” on a real server when we deploy the application.

Before we can do that, I’ve already created the commands (npm scripts) that we’ll need to run to get everything running locally for development and also built for production.

Adjust your "scripts" property to look like this:

"scripts": {
  "build": "rimraf dist && tsc",
  "preserve": "npm run build",
  "serve": "cross-env NODE_ENV=development concurrently \"tsc --watch\" \"nodemon -q dist/index.js\"",
  "prestart": "npm run build",
  "start": "cross-env NODE_ENV=production node dist/index.js",
  "test": "echo \"Error: no test specified\" && exit 1"
},

Let’s walk through these commands in detail, so you can understand what’s going on:

  • "build" is used in a few places so let’s start here. When we npm run build our rimraf package will delete our existing dist folder, ensuring no previous files exist. Then, we run tsc to build our project, which as we know is compiled into dist (remember we specified this in our tsconfig.json). i.e. deletes the old code and replaces it with the new code.

  • "preserve" just calls our "build" command to clean up any existing dist folders and recompiles via tsc. This gets called before the "serve" command, when we run npm run serve (which we’ll use to develop on localhost).

  • "serve" uses our cross-env package to set the NODE_ENV to development, so we know we’re in dev mode. We can then access process.env.NODE_ENV anywhere inside our .ts files should we need to. Then, using concurrently we’re running tsc --watch (TypeScript Compiler in “watch mode”) which will rebuild whenever we change a file. When that happens, our TypeScript code is outputted in out dist directory (remember we specified this in our tsconfig.json). Once that’s recompiled, nodemon will see the changes and reload dist/index.js, our entry-point to the app. This gives us full live recompilation upon every change to a .ts file.

  • "prestart" runs the same task as "preserve" and will clean up our dist, then use tsc to compile a new dist. This happens before the "start" is kicked off, which we run via npm start.

  • "start" uses cross-env again and sets the NODE_ENV to production, so we can detect/enable any “production mode” features in our code. It then uses node dist/index.js to run our project, which is already compiled in the "prestart" hook. All our "start" command does is execute the already-compiled TypeScript code.

  • "test" pffft. What tests.

It’s a lot to take in, read it a few times if you need to, but that’s about it.

Now we know this, let’s create some files and get things up and running.

Creating an index.ts file

Otherwise known as our “entry-point” to the application, we’ll be using index.ts.

📢 You can use another filename if you like, such as app.ts, however index.ts is a naming convention I prefer as it is also the default export for a folder. For example importing /src would be the same as importing /src/index. This cleans things up and can neatly arrange code in larger codebases, in my opinion.

Now, in the root of the project, create a src folder (or use mkdir src) and then create an index.ts with this inside (so you have src/index.ts):

console.log('Hello TypeScript');

Ready to compile and run TypeScript? Let’s go!

npm run serve

You should see some output like this (notice preserve, build, serve are executed in the specific order):

> [email protected] preserve
> npm run build

> [email protected] build
> rimraf dist && tsc

> [email protected] serve
> cross-env NODE_ENV=development concurrently "tsc --watch" "nodemon -q dist/index.js"

[0] Starting compilation in watch mode...
[1] Hello TypeScript

Boom! We’ve done it. It seems a lot to get going, but I’ve wanted to show you how to do this from scratch. There’s nothing more for us to “setup” now, we can get coding and both our local development and deployed application are ready to roll.

Now, change console.log('Hello TypeScript') to console.log('Hello JavaScript') and you’ll see things recompile:

[0] File change detected. Starting incremental compilation...
[0] Found 0 errors. Watching for file changes.
[1] Hello JavaScript

Mission success. We’ve now setup a local development environment that’ll recompile everytime we save a *.ts file and then nodemon will restart our dist/index.js file.

Adding Express.js

Let’s get Express setup and a server listening on a port.

We’ve actually got everything installed already, including any @types we might need. Amend your index.ts to include the following to test out your new Express server:

import express, { Express, Request, Response } from 'express';
import bodyParser from 'body-parser';
import helmet from 'helmet';
import dotenv from 'dotenv';

dotenv.config();

const PORT = process.env.PORT || 3000;
const app: Express = express();

app.use(helmet());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.get('/', (req: Request, res: Response) => {
  res.send('<h1>Hello from the TypeScript world!</h1>');
});

app.listen(PORT, () => console.log(`Running on ${PORT} ⚡`));

Now visit localhost:3000 in your browser and you should see the text Hello from the TypeScript world!.

This is because we’ve setup an app.get('/') with Express to return us some basic HTML.

TypeScript to ES5

Inside our tsconfig we’ve set the compiler option "target": "es5", so if you actually jump into your dist/index.js file, you’ll see the “transpiled” code, which is then fully compatible with most Node.js environments.

JavaScript Array Methods eBook Cover

🎉 Download it free!

Ready to go beyond ForEach? Get confident with advanced methods - Reduce, Find, Filter, Every, Some and Map.

  • Green Tick Icon Fully understand how to manage JavaScript Data Structures with immutable operations
  • Green Tick Icon 31 pages of deep-dive syntax, real-world examples, tips and tricks
  • Green Tick Icon Write cleaner and better-structured programming logic within 3 hours

As an extra bonus, we'll also send you some extra goodies across a few extra emails.

This is why using TypeScript is a great choice, as we can use many modern features that aren’t available just yet on Node.js environments (and even in browsers, TypeScript isn’t just for Node.js).

Here’s the code that tsc creates from our index.ts file, this is known as “emitting” in TypeScript talk:

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var express_1 = __importDefault(require("express"));
var body_parser_1 = __importDefault(require("body-parser"));
var helmet_1 = __importDefault(require("helmet"));
var dotenv_1 = __importDefault(require("dotenv"));
dotenv_1.default.config();
var PORT = process.env.PORT || 3000;
var app = express_1.default();
app.use(helmet_1.default());
app.use(body_parser_1.default.json());
app.use(body_parser_1.default.urlencoded({ extended: true }));
app.get('/', function (req, res) {
    res.send('<h1>Hello from the TypeScript world!</h1>');
});
app.listen(PORT, function () { return console.log("Running on " + PORT + " \u26A1"); });

It’s pretty similar, but you’ll notice it’s just ES5 which can run pretty much everywhere with no problems.

One of my biggest gripes with Node.js (especially with Serverless Lambda functions) is having to use the CommonJS require('module-name') syntax and lack of import/export keywords, which you’ll see our ES5 version “backports”. Meaning, we get to write shiny new TypeScript, a superset of JavaScript, without having to worry too much about different environments.

Now you’re ready to get started with your Node.js and TypeScript development. Go build and I’d love to hear on Twitter how you get on, give me a follow and check out my TypeScript courses if you haven’t already.

Though, we’re not done just yet… We still need to push our final project to GitHub.

Pushing to GitHub

In the project root, create a .gitignore file (with the dot prefix) and add the following values like so (then save and exit the file):

node_modules
dist
.env

A .gitignore file will stop us pushing those folders, and their containing files, to GitHub. We don’t want to push our huge node_modules folder, that’s pretty bad practice and defeats the purpose of dependencies.

Similarly, we don’t want to push our compiled dist directory.

With Node.js development you’ll also likely use a .env file as well, so I’ve included that as a default too (even though we’re not using one here).

👻 The .env file is used to keep secrets such as API keys and more, so never push these to version control! Set up the .env locally then mirror the variables on your production environment too.

Now time to commit our code to GitHub!

Run each of these (line by line):

git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/ultimatecourses/node-express-typescript.git
git push -u origin main

Now head over to your Node + TypeScript + Express GitHub project and check out your finished masterpiece.

🙌 If you want to learn even more, I’ve built a bunch of TypeScript Courses which might just help you level up your TypeScript skills even further. You should also subscribe to our Newsletter!

Deploying your Node.js app

Once you’ve built out your application you can simply push it to a service. I like to use something like Heroku, where upon each commit to the GitHub repo it will automatically deploy a new version.

Remember, all you need is to have your production environment run npm start, and the project will build and execute the ES5 code. And that’s it! You’re fully setup and ready to go.

I hope you learned tonnes in this article, it was certainly fun breaking it all down for you.

Happy TypeScripting!

Related blogs 🚀