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!
Table of contents
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 thedist
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 incomingrequest
stream (for Express) and exposes it onreq.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 intoprocess.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 ourdist
directory before recompiling a newdist
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.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
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 wenpm run build
ourrimraf
package will delete our existingdist
folder, ensuring no previous files exist. Then, we runtsc
to build our project, which as we know is compiled intodist
(remember we specified this in ourtsconfig.json
). i.e. deletes the old code and replaces it with the new code. -
"preserve"
just calls our"build"
command to clean up any existingdist
folders and recompiles viatsc
. This gets called before the"serve"
command, when we runnpm run serve
(which we’ll use to develop onlocalhost
). -
"serve"
uses ourcross-env
package to set theNODE_ENV
todevelopment
, so we know we’re in dev mode. We can then accessprocess.env.NODE_ENV
anywhere inside our.ts
files should we need to. Then, usingconcurrently
we’re runningtsc --watch
(TypeScript Compiler in “watch mode”) which will rebuild whenever we change a file. When that happens, our TypeScript code is outputted in outdist
directory (remember we specified this in ourtsconfig.json
). Once that’s recompiled,nodemon
will see the changes and reloaddist/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 ourdist
, then usetsc
to compile a newdist
. This happens before the"start"
is kicked off, which we run vianpm start
. -
"start"
usescross-env
again and sets theNODE_ENV
toproduction
, so we can detect/enable any “production mode” features in our code. It then usesnode 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
, howeverindex.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.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
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!