Node, Typescript and Express routers as classes

In this post I will present my solution for writing Express.js routers as classes in Typescript. I will also give you a sneak peek of my architecture, which I believe makes the application components easily testable as well as microservice-ready.

I personally love Typescript. It makes my code less error-prone and I think it greatly improves the development experience. That’s why I started using it for my node.js app.

I will use a support ticket feature as an example, which will be a restful API that allows users to perform basic CRUD operations for support tickets. However this post will only cover the route for retrieving a ticket.

My editor of choice is Visual Studio Code. It provides excellent Typescript support and a large varity of themes and extensions.

Getting started

The public property “router” is the express router of my router class. When I instantiate the entire app, I can assign a sub route for the ticket component by accessing the router property when the application boots. I could, for instance, do express.use('/tickets', new TicketComponent(new TicketService()).router) when bootstraping the application. This decouples my components from the rest of the application.

My TicketComponent.ts file below is responsible for exposing the HTTP endpoints for my ticket API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Router, Response, NextFunction } from 'express'
import TicketService from './TicketService';

export default class TicketComponent {

public router: Router;
private ticketService: TicketService:

constructor(service: TicketService) {
this.router = Router();
this.ticketService = service;
this.routes();
}

private routes() {
this.router.get('/:id', this.read);
}

private read = async (req: Request, res: Response, next: NextFunction) => {
try {
let ticket = await this.ticketService.get(req.params.id);
res.send(ticket);
} catch(err) {
next(err)
}
}
}

As you can see, the constructur requires a TicketService to be injected in order to create an instance. The ticket service handles all the business logic related to the component. The business logic is seperated from Express so that I can easily switch out Express if neccessary, or use the same logic for other protocols such as web sockets.

The ticket service is a dependency that will now be shared by all the routes through this.ticketService. But, this is where things can get a little tricky. A common mistake is to define the routes as methods in the router class. This will lead to this being undefined when the route is accessed.

1
2
3
4
5
6
7
8
9
10
11
12
private routes() {
this.router.get('/:id', this.read);
}

private async read(req: Request, res: Response, next: NextFunction) {
try {
let ticket = await this.ticketService.get(req.params.id);
res.send(ticket);
} catch(err) {
next(err)
}
}

This will end in a “TypeError: Cannot read property ‘ticketService’ of undefined”. I can confirm that it is undefined with console.log(this) which gives “undefined”. The value of this inside a function depends on how the function is called. When Express calls the function (the route), it has no idea what this is since it has not been passed along with the function. The same error would occur if the function is assigned to a property of the class.

Solution #1

The first solution is to bind this to the read method, which will make the router component instance available wherever the method is sent.

1
2
3
4
5
6
7
8
9
10
11
12
private routes() {
this.router.get('/:id', this.read.bind(this));
}

private async read(req: Request, res: Response, next: NextFunction) {
try {
let ticket = await this.ticketService.get(req.params.id);
res.send(ticket);
} catch(err) {
next(err)
}
}

By doing so, this will no longer be undefined, and also reference the TicketComponent-object.

Solution #2

I find it more readable to utilize arrow functions to prevent this from being undefined. You will have to use Typescript or Babel for this to work.

1
2
3
4
5
6
7
8
9
10
11
12
private routes() {
this.router.get('/:id', this.read);
}

private read = async (req: Request, res: Response, next: NextFunction) => {
try {
let ticket = await this.ticketService.get(req.params.id);
res.send(ticket);
} catch(err) {
next(err)
}
}

Arrow functions keep the enclosing lexical context of this, which means that this in the function will reference whatever it referenced when it was defined. I believe the routes() method to be more readable as I add more routes if I do it like this.

Using arrow functions as properties comes with some caveats. Arrow functions in class properties are not in the prototype, which means other classes cannot inherit the arrow function property. More of these caveats are covered in depth in this article.

Let me know in the comment section below what you think. Feel free to correct me if I’m wrong about something.
Do you want me to turn this into a series where I cover more parts of my application?