It was early. I was one of the first ones in the office. I made a coffee and I sat down at my work station. I opened Jira to pull a new story into progress. I opened the IDE and cloned the project. I was delighted to see it was using TypeScript. This feeling was going to change very soon.

I started by installing the dependencies. I was planning to run the project. I opened package.json with the hope that I would find a script that would do that. It was there but it was starting the transpiled javascript files. These files were not yet created. I figured I had to run the compile script first. I did that and then I was finally able to run the project. This didn't seem very cool to me but I could live with it.

I moved on. I opened one of the TypeScript controllers I was interested in. I put a breakpoint on the first line and started the project in debug mode. I sent a request. The process didn't pause on the breakpoint. Oh wait, that's right. It's because we are running the javascript files. That means we can only debug those. I did not like this at all.

This task was quite urgent. I had to get over my disappointment and add the changes. After all, it's not very hard to figure out which functionality in TypeScript is corresponding to which functionality in JavaScript. I did the small changes that needed to be done for the task and ran the tests. They were all passing. They shouldn't have been.

We had only a few unit tests that were mocking almost everything. They were not the most useful tests. I added a test for my change. I committed my changes and pushed them to the code repo.

All in all, the project was not as bad as it might seem. It could have been improved though. I wanted to do it but I had never built a setup for express with TypeScript before. This was going to change in the following week. What follows is the story of how I did it and what I learned along the way.

The obvious ones

TypeScript

I knew what the first step must be. I needed to initialize the project

npm init -y

This was simple enough. What now? Well, it was obvious that I needed to install Typescript and configure it. So let's just do that

npm install --save-dev typescript

Let's see if this works.

npx tsc

The command executed successfully so it seemed to work.

Let's initialize the TypeScript project now.

npx tsc -init

This is creating a file called tsconfig.json. It contains the settings for TypeScript. I uncommented the line that contained the key outDir and I made sure its value was set to ./dist

{
  "compilerOptions": {
    "outDir": "./dist"
  }
}
tsconfig.json

I was planning to have the source files in the src directory. After a bit of research, I figured I had to add an include setting to tsconfig.json. I modified the file and I added the following field just after the compilerOptions.

{
  "include": ["src/**/*"]
}
tsconfig.json

I wanted to be able to debug TypeScript files so I had to uncomment the line that contained the sourceMap field as well

{
  "compilerOptions": {
    "sourceMap": true
  }
}
tsconfig.json

It seemed I had figured out the TypeScript part.  Next was the other obvious thing. I had to setup express.

Express

A lot of questions were popping in my head at this time. How am I going to make express work with the type system from TypeScript? Can I write controllers as classes? I want to have services that contain my business logic. How am I going to add them as dependencies for the controllers? How about testing? What are the options there? Soon I learned the answer to all of these questions.

I started by installing express

npm install --save express

I wanted to make the project work with the type system from TypeScript. I just had to install the types for express and node

npm install --save-dev @types/express
npm install --save-dev @types/node

I now had to add scripts to the package.json for compiling TypeScript files and starting the project

"scripts": {
  "compile": "npx tsc",
  "start": "npm run compile && node ./dist/main.js"
}
package.json

As you can see the start script is calling the compile script first. No surprises when someone starts the project for the first time.

So far I was happy to see the pieces of the puzzle falling into place. How about when developing on the project? Do I want to restart the project every time I make a change? It would be cool to add nodemon to have it watch over my changes and restart the project automatically for me.

I installed nodemon for that

npm install --save-dev nodemon

Then I had to add a script to the package.json for starting the project for development

{
  "scripts": {
    "start-dev": "node ./node_modules/nodemon/bin/nodemon.js -e ts --exec \"npm run start\""
  }
}
package.json

Linting

It's always cool to play by the rules of the language. When you start out it's quite hard to get everything right. The IDE is helping a lot in this respect but you can't make sure everyone has the same IDE setup. This is where I see linting to be useful. This was a nice to have though. I think these nice to haves are always welcome on projects so I've added linting as well.

I installed tslint

npm install --save-dev tslint

I created the tslint.json for configuring the linter

{
  "extends": ["tslint:recommended"],
  "linterOptions": {
    "format": "verbose"
  }
}
tslint.json

Then I added a lint command to package.json

{
  "scripts": {
    "lint": "npx tslint --project tsconfig.json --config tslint.json"
  }
}
package.json

Testing

Testing is one of the most critical parts of the development process. Not only it makes sure your code works, but it also gives you peace of mind while refactoring. It ensures that your code has a better structure as well. All of these add a lot to the code quality. I wanted to have multiple types of tests. I usually write unit, integration, and functional tests. Most of my tests being integration tests.

A good option for testing these days seems to be jest. This is also working with TypeScript. I was in luck. I installed it toghether with the TypeScript types and the jest TypeScript preprocessor ts-jest

npm install --save-dev jest
npm install --save-dev @types/jest
npm install --save-dev ts-jest

Next I had to configure jest.

module.exports = {
    "roots": ["src", "tests"],
    "transform": {"^.+\\.tsx?$": "ts-jest"}
}
jest.config.js

This allowed me to have the source files in the src directory and the test files in the tests directory. I also added the transform setting for calling the preprocessor for TypeScript files. The ts-jest module also has support for source maps which allowed me to use the debugger when writing tests.

Then I added the commands for testing to the scripts in package.json

{
  "scripts": {
    "test": "jest",
    "test-with-coverage": "jest --coverage"
  }
}
package.json

As you can see, jest comes with test coverage support which is super cool. It's always nice having a look at the coverage numbers before committing my work.

This is covering the unit and integration part of my testing. How about the functional tests? That's where supertest enters the picture. This module can send HTTP requests to your application from the test environment. Let's install this as well then

npm install --save-dev supertest @types/supertest

Taking it for a spin

Now that I had the setup pretty much figured out, I needed to see what the experience of developing something using this setup felt like. I planned to create a toy of an api that would return a greeting.

I wanted to start by writing something simple. I did not handle the express setup first because I knew that had the potential to change after writing the controller and the service. I started with the service

export class GreetingService {
    public greetNewcomer() {
        return 'Welcome home!';
    }
}
src/services/greeting.service.ts

There is nothing easier than this. Let's write a unit test for it

import {GreetingService} from "../../../src/services/greeting.service";

describe('GreetingService', () => {
    let greetingService: GreetingService;

    beforeEach(() => {
        greetingService = new GreetingService();
    });

    it('is created', () => {
        expect(greetingService).toBeInstanceOf(GreetingService);
    });

    describe('#greetNewcomer', () => {
        it('returns the newcomer greeting', () => {
            expect(greetingService.greetNewcomer()).toEqual('Welcome home!');
        });
    })
});
tests/unit/greeting.service.test.ts

This is testing that the service can be instantiated and that the method is returning the right greeting.

Next thing on the list was adding a controller that would call the greeting service

import {GreetingService} from "../services/greeting.service";

export class HomeController {
    public constructor(private greetingService: GreetingService) {
    }

    public index() {
        return this.greetingService.greetNewcomer();
    }
}
src/controllers/home.controller.ts

Cool, but how was I going to pass the GreetingService dependency to this controller? I didn't want to instantiate the service myself and then pass it when I instantiate the controller. I wasn't even considering instantiating the service in the controller. This is something that dependency injection should do for me. After a bit of research, I found a module called typedi that does dependency injection for TypeScript. I installed it and its reflect-metadata dependency

npm install typedi --save
npm install reflect-metadata --save

Then I had to enable the following settings in tsconfig.json

{
  "emitDecoratorMetadata": true,
  "experimentalDecorators": true
}
tsconfig.json

This is enabling decorators for the project. The dependency injection module needs this. Little did I know that later on, I needed the decorators for the controllers as well.

While researching the dependency injection I stumbled upon a very cool module for routing requests to controllers in TypeScript with express that is using typedi. I installed it together with all its dependencies

npm install --save routing-controllers
npm install --save body-parser multer
npm install --save-dev @types/body-parser @types/multer
npm install --save class-transformer class-validator

Then I refactored the home controller to use the appropriate decorators from routing-controllers

import {GreetingService} from "../services/greeting.service";
import {Controller, Get} from "routing-controllers";

@Controller()
export class HomeController {
    public constructor(private greetingService: GreetingService) {
    }

    @Get("/")
    public index() {
        return this.greetingService.greetNewcomer();
    }
}
src/controllers/home.controller.ts

This is declaring the HomeController as being a Controller. It also makes the index method be called when a GET request to / is sent to the api.

I now had to tackle the express integration. I had to create the express app so that I could pass it to routing-controllers

import "reflect-metadata";
import {useContainer, useExpressServer} from "routing-controllers";
import {Container} from "typedi";
import {HomeController} from "./controllers/home.controller";
import express, {Express} from "express";

useContainer(Container);

const app: Express = express();
useExpressServer(app, {
    controllers: [
        HomeController
    ]
});

export default app;
src/app.ts

The line where the reflect-metadata is imported is essential. Without it, the dependency injection and the decorators for the routing-controllers would not work. Another important aspect is that I had to register the container class to be used by the routing-controllers.

Now that I created the app I could make it listen on port 3000

import app from './app';

app.listen(3000);
src/main.ts

In an ideal world, I would have tested the controller before I did all these. Somehow I found it easier this way though.

Speaking about tests, I thought a good option to test the dependency injection was to start with the service again so I created an integration test for that

import {Container} from "typedi";
import {GreetingService} from "../../../src/services/greeting.service";

describe('GreetingService', () => {
    let greetingService: GreetingService;

    beforeEach(() => {
        greetingService = Container.get(GreetingService);
    });

    it('is created', () => {
        expect(greetingService).toBeInstanceOf(GreetingService);
    });
});
tests/integration/services/greeting.service.test.ts

Well, it did not work. I figured it must be from the reflect-metadata not being imported for tests. I remembered afterward that I also had to register the container for tests. I wanted to have this on a global level and not import it in every test. I added a jest setup file to jest config

"setupFiles": [
  "./jest.setup.ts"
]
jest.config.js

In the global setup file I imported the reflect-metadata and I registered the container in the routing-controllers for tests as well

import "reflect-metadata";
import {Container} from "typedi";
import {useContainer} from "routing-controllers";
useContainer(Container);
jest.setup.ts

This time the service integration test passed. Then I though it would be cool to add an integration test for the controller as well

import {Container} from "typedi";
import {HomeController} from "../../../src/controllers/home.controller";

describe('HomeController', () => {
    let homeController: HomeController;

    beforeEach(() => {
        homeController = Container.get(HomeController);
    });

    it('is created', () => {
        expect(homeController).toBeInstanceOf(HomeController);
    });

    describe('#index', () => {
        it('returns the welcome message', () => {
            expect(homeController.index()).toEqual('Welcome home!');
        });
    });
});
tests/integration/controllers/home.controller.test.ts

As you can see I get the HomeController from the dependency injection container. This allows me not to be worried about its dependencies and potentially instantiating those as well.

How about a functional test? I would like to send an HTTP request to the api to see that it works. This is where supertest enters the scene

import request from 'supertest';
import app from "../../../src/app";

describe('Home', () => {

    function callHome() {
        return request(app)
            .get('/');
    }

    it('returns welcome greeting', async () => {
        const received = await callHome();
        expect(received.status).toEqual(200);
        expect(received.text).toEqual("Welcome home!");
    });
});
tests/functional/home.test.ts

I'm doing a GET request to / and expect that I would get "Welcome home!". Well, it works.

I am quite pleased with the result and with the development experience. I will undoubtedly use this setup for a side project or at work. There are some other aspects that I would like to experiment with. I would like to see how this works with a persistence layer for example. I will let you know how that went when I get to it.

See you soon

This was my adventure creating a setup for express with TypeScript. If you want to see the final version of it, you can find it at https://github.com/thelexned/typescript-express