Routing with TypeScript decorators for node applications
Tags: typescript, node
Published: 2019-06-25 16:56:48

---
title: Routing with TypeScript decorators for node applications
published: true
tags: TypeScript,Node.js
canonical_url: https://nehalist.io/routing-with-typescript-decorators/
---

 ![Routing with TypeScript decorators for node applications](https://ghost.nehalist.io/content/images/2019/06/typescript-decorators-routing.png)

Decorators can be a useful tool when writing TypeScript applications. One way I like to use them for is for creating node-based MVC web application where decorators provide a convenient tool for routing. Think of something like this:

```typescript
@Controller('/user')
class UserController {
    @Route('/')
    public index() {
        // return proper response
    }
    
    @Route('/:name')
    public details() {
        // return proper response
    }
}
```

This would provide routes prefixed with `/user`, while `/user` may lead to a user overview and `/user/:name` lead to user details. This concept may look familiar if you're used to [Symfony Routing](https://symfony.com/doc/current/routing.html) or [NestJS Controllers](https://docs.nestjs.com/controllers).

## Introduction

In this post we're going to build an [Express](http://expressjs.com/) application with TypeScript using [ts-node](https://www.npmjs.com/package/ts-node) (which makes it possible to run TypeScript applications without having to compile our `.ts` files manually).

You could use any underlying framework you want, e.g. [koa](https://koajs.com/), [hapi](https://hapijs.com/) or even your very own http server implementation. Differences should only occur when actually registering route, everything else should remain the same. The only reason why I've chosen express is because I have the most experience with it - but feel free to use whatever _you_ want.

In case you're looking for a TL;DR: you can find the [entire source code for this post on GitHub](https://github.com/nehalist/ts-decorator-routing).

## Architecture

There are different ways to implement routing capabilities with TypeScript. But before diving right into the implementation it's important to keep some things in mind.

The first important information is:

> Decorators are called when a class is declared, not when it's instantiated.

So when decorating our method we do **not** have an instantiated object to work on inside our decorator. Instead we just have a class declaration which we can use. See [here](https://www.typescriptlang.org/docs/handbook/decorators.html#decorator-evaluation) for detailed information about decorator evaluation order.

Since decorators are just functions they have their own scope. This becomes a bit problematic as soon as we realize that route registration for express takes place outside decorators:

![Routing with TypeScript decorators for node applications](https://ghost.nehalist.io/content/images/2019/03/routing-arch-2.png)

One way to get our routes from the decorator to our express application would be introducing a registry class which would be filled by our decorator and read at some later point when registering our routes.

**But** there's an easier way to do this which involves the [reflect-metadata](https://github.com/rbuckton/reflect-metadata) library (which you're likely already using if you're dealing with decorators). Instead of using a separate layer (in form of a registry) we could simply attach routes to our controller metadata:

![Routing with TypeScript decorators for node applications](https://ghost.nehalist.io/content/images/2019/03/routing-metadata.png)

We simply save routes to our controller metadata. Later, when registering our routes in our express application, we already need to load our controllers - and that's where we simply read our route metadata and register them properly.

Knowing all of these things let's start implementing our routing decorators!

## Express application

First of all we need to create our express application. In our first iteration we'll just serve a default route to test if everything works:

```typescript
// index.ts

import 'reflect-metadata';
import {Request, Response} from 'express';

const app = express();

app.get('/', (req: Request, res: Response) => {
  res.send('Hello there!');
});

app.listen(3000, () => {
  console.log('Started express on port 3000');
});
```

> `reflect-metadata` only needs to be imported once per application, hence here's a good place to do so.

Start your server with `ts-node index.ts` and head over to `localhost:3000` to be friendly greeted by [Obi-Wan](https://www.youtube.com/watch?v=rEq1Z0bjdwc).

## Controller decorator

This decorator will be attached to our controllers and contain the prefix for this controller:

```typescript
// Decorator/Controller.ts

export const Controller = (prefix: string = ''): ClassDecorator => {
  return (target: any) => {
    Reflect.defineMetadata('prefix', prefix, target);

    // Since routes are set by our methods this should almost never be true (except the controller has no methods)
    if (! Reflect.hasMetadata('routes', target)) {
      Reflect.defineMetadata('routes', [], target);
    }
  };
};
```

A pretty simple class decorator which sets the `prefix` metadata on the controller and, in case no `routes` metadata has been found, sets it to an empty array. As stated in the comments `routes` should almost never be `undefined`, except our controller has no decorated methods.

## Route decorator

It would be convienient to have a decorator for every HTTP verb, like `@Get`, `@Post`, etc.. For the sake of simplicity we're only implement the `@Get` decorator:

```typescript
// Decorator/Get.ts

import {RouteDefinition} from '..';

export const Get = (path: string): MethodDecorator => {
  // `target` equals our class, `propertyKey` equals our decorated method name
  return (target, propertyKey: string): void => {
    // In case this is the first route to be registered the `routes` metadata is likely to be undefined at this point.
    // To prevent any further validation simply set it to an empty array here.
    if (! Reflect.hasMetadata('routes', target.constructor)) {
      Reflect.defineMetadata('routes', [], target.constructor);
    }

    // Get the routes stored so far, extend it by the new route and re-set the metadata.
    const routes = Reflect.getMetadata('routes', target.constructor) as Array<RouteDefinition>;

    routes.push({
      requestMethod: 'get',
      path,
      methodName: propertyKey
    });
    Reflect.defineMetadata('routes', routes, target.constructor);
  };
};
```

> **Important:** It's important to set our target `target.constructor` and not just `target` to properly handle metadata.

Again, a pretty simple decorator which extends the stored routes on the controller by a new route. `RouteDefinition` is an interface which defines the shape of our routes:

```typescript
// Model/RouteDefinition.ts

export interface RouteDefinition {
  // Path to our route
  path: string;
  // HTTP Request method (get, post, ...)
  requestMethod: 'get' | 'post' | 'delete' | 'options' | 'put';
  // Method name within our class responsible for this route
  methodName: string;
}
```

> **Tip:** In case you'd like to add some middleware before controllers it'd be a good idea to store it within the `RouteDefinition`.

Now we've got both of our required decorators and can get back to our express application to register our routes.

## Registering routes

Before registering our routes to our express application let's implement a controller with our new decorators:

```typescript
// UserController.ts

import {Controller} from '../src';
import {Get} from '../src';
import {Request, Response} from 'express';

@Controller('/user')
export default class UserController {
  @Get('/')
  public index(req: Request, res: Response) {
    return res.send('User overview');
  }

  @Get('/:name')
  public details(req: Request, res: Response) {
    return res.send(`You are looking at the profile of ${req.params.name}`);
  }
}
```

Heading to `/user` should show a "User overview" message and `/user/foobar` should show a "You are looking at the profile of foobar"message.

But before this fully works we need to tell express about our routes - so let's get back to our `index.ts`:

```typescript
import 'reflect-metadata';
import * as express from 'express';
import UserController from './example/UserController';
import {RouteDefinition} from './src';

const app = express();

app.get('/', (req: express.Request, res: express.Response) => {
  res.send('Hello there!');
});

// Iterate over all our controllers and register our routes
[
  UserController
].forEach(controller => {
  // This is our instantiated class
  const instance = new controller();
  // The prefix saved to our controller
  const prefix = Reflect.getMetadata('prefix', controller);
  // Our `routes` array containing all our routes for this controller
  const routes: Array<RouteDefinition> = Reflect.getMetadata('routes', controller);
  
  // Iterate over all routes and register them to our express application 
  routes.forEach(route => {
    // It would be a good idea at this point to substitute the `app[route.requestMethod]` with a `switch/case` statement
    // since we can't be sure about the availability of methods on our `app` object. But for the sake of simplicity
    // this should be enough for now.
    app[route.requestMethod](prefix + route.path, (req: express.Request, res: express.Response) => {
      // Execute our method for this path and pass our express request and response object.
      instance[route.methodName](req, res);
    });
  });
});

app.listen(3000, () => {
  console.log('Started express on port 3000');
});
```

And voilà, that's it. We now can navigate to `/user` or `/user/<name>` and get proper responses from our express application. _Wohoo!_

## Advancing this approach

This is a very basic approach which contains lots of room for improvement when it comes to the actual implementation. Here are some thoughts on improving this implementation:

### Instantiation

Our controller is naively instantiated with `new controller()`. But what if our controller does have some `constructor` arguments?

This would be a perfect use case to apply some [dependency injection as described in a former post](https://nehalist.io/dependency-injection-in-typescript/) which would be plugged in right at where our controller is instantiated.

### Return values

I'm not a huge fan of `res.send()` - instead it would be pretty convinient if responses could be simple objects which reflects their content (think of something like `return new JsonResponse(/* ... */)`). This could easily be done by implementing such response objects and return them from our methods - later, when registering our routes, the callback would still `send` our response:

```
app[route.requestMethod](prefix + route.path, (req: express.Request, res: express.Response) => {
  const response = instance[route.methodName](req, res);
  res.send(response.getContent()); // where `getContent` returns the actual response content
});
```

This would require additional validation of our return value, especially to prevent express being stuck due to the lack of a `next()` call (which is covered by `send` but needs to be called manually if you don't utilise `send`).

## Conclusion

As you've just seen it's pretty simple to handle routing via TypeScript decorators without having too much magic going on or having to install frameworks or any additional libraries.

As always the [entire source code (including tests) for this post can be found on GitHub](https://github.com/nehalist/ts-decorator-routing).

---

If you liked this post feel free to leave a ❤, follow me on [Twitter](https://twitter.com/nehalist) and subscribe to [my newsletter](https://nehalist.io/#subscribe). This post was originally published at [nehalist.io](https://nehalist.io/routing-with-typescript-decorators/) on April 2, 2019.