What are we going to build?
We are going to create a TypeScript project for writing an Express.js API. We will start by setting everything up, mixing in TSLint, adding Mocha and Chai for tests, and will then use webpack to create a bundled JS file, which can be run with node, out of the box, without anything else needed.
Some quick notes
I'm going to use Yarn here, but if you wish to use npm instead, feel free to change commands accordingly.
I use zsh on Mac OS X so what you see in your terminal may actually vary from what you are seeing in this post. The outputs and results of each individual command, however, should still be the same.
Initial setup
We will start by creating a new directory and setting up a new project:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript]> mkdir express-api
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript]> cd express-api
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn init --yes
yarn init v1.12.3
warning The yes flag has been set. This will automatically answer yes to all questions, which may have security implications.
success Saved package.json
✨ Done in 0.03s.
(Feel free to omit Yarn's --yes
parameter on init and fill out everything to your liking.)
Adding TypeScript, TSLint and Express
Now that the basic project is set up, it's time to add the first dependencies, TypeScript and TSLint:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn add --dev typescript tslint
yarn add v1.12.3
info No lockfile found.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 📃 Building fresh packages...
[...]
✨ Done in 1.96s.
and express, as well as its types:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn add express
yarn add v1.12.3
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 📃 Building fresh packages...
[...]
✨ Done in 0.90s.
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn add --dev @types/express @types/node
yarn add v1.12.3
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 📃 Building fresh packages...
[...]
✨ Done in 0.96s.
Nearly there with our most basic setup. TypeScript will need a tsconfig.json
now and TSLint will get its tslint.json
next.
The basic config TSLint's init provides is more than enough for now and an empty tsconfig.json
, which we will fill afterwards:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn tslint --init
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> touch tsconfig.json
TypeScript's tsc init
would produce a relatively large config with many settings commented out, to showcase what's possible, but as we are pretty sure what we need, we will use the following tsconfig.json
:
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": ["node_modules/*"]
}
},
"include": ["src/**/*"]
}
The only thing that is left for us, is to create a src/
directory, add our first ts file
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> mkdir src
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> touch src/index.ts
and put the following contents into index.ts
:
import express from "express";
const app = express();
const port = 8080;
app.get("/", (req, res) => {
res.send("Hello world!");
});
app.listen(port, () => {
console.log(`server started at http://localhost:${port}`);
});
Testing our initial setup
We're set up. We can now compile our app and start our server like this:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn tsc
yarn run v1.12.3
$ /Users/oliver/projects/typescript/express-api/node_modules/.bin/tsc
✨ Done in 2.02s.
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> node dist/index.js
server started at http://localhost:8080
Following the link will give us a friendly Hello world!
within our browser. Great!
Adding npm scripts
We are not making use of TSLint, yet, and executing each command manually with Yarn is not the best use of our time, so we are going to add some npm scripts and change our package.json
, while also letting main
point to our dist/index.js
.
{
"name": "express-api",
"version": "1.0.0",
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
"build": "tsc",
"prestart": "yarn build",
"start": "node ."
},
"devDependencies": {
"@types/express": "^4.17.1",
"@types/node": "^12.7.5",
"tslint": "^5.20.0",
"typescript": "^3.6.3"
},
"dependencies": {
"express": "^4.17.1"
}
}
We only need one command now to see our new API in action:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn start
yarn run v1.12.3
$ tslint -c tslint.json -p tsconfig.json --fix
ERROR: /Users/oliver/projects/typescript/express-api/src/index.ts:11:5 - Calls to 'console.log' are not allowed.
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Whoops. TSLint does not like that we are using the console to signal that our server is up. Let's quickly fix that by changing the contents of tslint.json
to the following:
{
"defaultSeverity": "error",
"extends": ["tslint:recommended"],
"jsRules": {},
"rules": {
"no-console": [false]
},
"rulesDirectory": []
}
When we try it once again:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn start
yarn run v1.12.3
$ yarn build
$ tslint -c tslint.json -p tsconfig.json --fix
$ tsc
$ node .
server started at http://localhost:8080
voilà, it works!
Adding Mocha and Chai for testing
Until now, we have done everything that we need to be able to implement our API and let it run on our local machine.
It's time to add some testing capabilities now.
As stated at the beginning, we're going to use Mocha and Chai. We will also need ts-node for Mocha to work with TypeScript correctly.
Another round of adding dependencies:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn add --dev mocha chai ts-node @types/mocha @types/chai
yarn add v1.12.3
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 📃 Building fresh packages...
[...]
✨ Done in 0.96s.
Perfect, we can now create our first spec index.spec.ts
within src/
and add the following contents to it:
import { expect } from "chai";
import "mocha";
describe("This", () => {
describe("should", () => {
it("always pass", () => {
expect(true).to.equal(true);
});
});
});
This is our first test and there are many more to come in the future.
But for now it would be great if our tests ran every time after the TypeScript compiler.
We can use postbuild
to let our tests run every time after the compiler ran within our package.json
like this:
{
"name": "express-api",
"version": "1.0.0",
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
"build": "tsc",
"postbuild": "yarn test",
"prestart": "yarn build",
"start": "node .",
"test": "mocha -r ts-node/register src/**/*.spec.ts"
},
"devDependencies": {
[...]
},
"dependencies": {
[...]
}
}
Now our tests are a part of the build chain.
webpack
Why webpack
We could stop right here, implement our API further and then deploy the whole project somewhere. That would be perfectly fine but it would be a bit inefficient.
Let's take a look at the size of our node_modules
:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> du -hs node_modules
67M node_modules
Well, that's a bit of disk space used, just for one endpoint which returns "Hello world!" and does nothing else, yet.
But webpack can help us reduce our app to the absolute necessary.
This comes in very handy, especially if we wanted to deploy our app to a Docker container, for example.
Adding webpack
Let's add all dependencies we need for webpack:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> yarn add --dev webpack-cli webpack ts-loader
yarn add v1.12.3
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 📃 Building fresh packages...
[...]
✨ Done in 7.35s.
Great. We will need a webpack config next, so that we can make use of its bundling and tree shaking.
We add the following to a file named webpack.config.js
:
const path = require('path');
module.exports = {
name: 'deployment',
mode: 'production',
entry: './src/index.ts',
target: 'node',
devtool: 'hidden-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
usedExports: true,
},
};
This is basically all we need and we are nearly good to go.
Let us now tweak our package.json
so that we use webpack instead of the plain TypeScript compiler:
{
"name": "express-api",
"version": "1.0.0",
"main": "dist/bundle.js",
"license": "MIT",
"scripts": {
"prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
"build": "webpack",
"postbuild": "yarn test",
"prestart": "yarn build",
"start": "node .",
"test": "mocha -r ts-node/register src/**/*.spec.ts"
},
"devDependencies": {
[...]
},
"dependencies": {
[...]
}
}
When we use yarn start
or yarn build
now, webpack will create a bundle.js
which is then executed instead of the old index.js
.
Let's check how large the dist/
folder is:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> du -hs dist
1.2M dist
1.2 megabytes! This is our bundle, including a source map. Let's take a look at the bundle, only:
┌[oliver@Olivers-MBP] [/dev/ttys002]
└[~/projects/typescript/express-api]> du -hs dist/bundle.js
564K dist/bundle.js
Pretty good and way better. 564K is everything we need. We could now take this file, drop it somewhere and start it with node bundle.js
. No need to take the whole project and drop it somewhere just to let the server run.
A few last tweaks to our npm scripts
There a three more scripts, we can add to our package.json
to make our life, while developing, a little easier.
Quick compile during development
webpack does, obviously, take more time than just compiling our code with the TypeScript compiler. That overhead could possibly add up during development. We can bypass webpack for such circumstances by using the TypeScript compiler directly and start our server afterwards by adding the following to our package.json
:
"scripts": {
[...]
"dev": "tsc",
"postdev": "node dist/index.js"
},
which enables us to start our server with just yarn dev
.
Cleaning up before we build
With our new yarn dev
script, we actually have two types of builds now that create different files. Before doing a production grade build, we should clean up our dist/
folder so that only the files we really need, in the end, are left, after building.
Let's change our prebuild
step by using a clear
script we put into our package.json
:
"scripts": {
"prebuild": "yarn clear && tslint -c tslint.json -p tsconfig.json --fix",
[...]
"clear": "rm -r dist/* || true"
}
Conclusion
That's it. We've built a pretty good starter to continue development on. All we need to deploy from now on is only our bundle.js
(and maybe our bundle.js.map
) instead of our whole project.
In case you couldn't exactly follow all steps, you can find the template here on GitHub.
Top comments (2)
Thanks Oliver, very didactic, learned a lot.
Thank you very much! :-)