DEV Community

loading...

Building a project with TypeScript, Express, Mocha and Chai

oliverjumpertz profile image Oliver Jumpertz ・7 min read

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.
Enter fullscreen mode Exit fullscreen mode

(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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`);
});

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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": []
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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": {
    [...]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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": {
    [...]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

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"
  }
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (2)

Collapse
douglaslondrina profile image
Douglas Schmidt

Thanks Oliver, very didactic, learned a lot.

Collapse
oliverjumpertz profile image
Oliver Jumpertz Author

Thank you very much! :-)

Forem Open with the Forem app