Introduction to Routing-Controllers

Table of contents

Routing Controllers is a framework for building backend applications in Node. It is quite lightweight, easy to learn and pleasant to use.

You can find their repo at https://github.com/typestack/routing-controllers.

This page will guide you in setting up a completely new web app that uses Routing Controllers, and will show you its main features.

You can use it with different web servers, but for this article I'm going to assume Express.

Installation

Although you only really need routing-controllers, there are a few other peer dependencies worth installing from the start.

yarn init -y;

yarn add routing-controllers reflect-metadata express body-parser multer\
    class-transformer class-validator ts-node typescript dotenv cross-env tslib;

yarn add -D @types/express @types/body-parser @types/multer @types/node\
    node-config tsconfig-paths nodemon;

And add a script to your package.json:

  "scripts": {
    "start": "npm run build && cross-env NODE_ENV=production node dist/server.js",
    "dev": "cross-env NODE_ENV=development nodemon"
  }

You will also want to update your tsconfig.json by adding these:

{
  "compileOnSave": false,
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "baseUrl": "src",
    "declaration": true,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "forceConsistentCasingInFileNames": true,
    "importHelpers": true,
    "lib": ["es2017", "esnext.asynciterable"],
    "module": "commonjs",
    "moduleResolution": "node",
    "noEmit": false,
    "outDir": "./dist",
    "pretty": true,
    "resolveJsonModule": true,
    "sourceMap": true,
    "target": "es2017",
    "typeRoots": ["node_modules/@types"]
  },
  "include": ["src/**/*.ts", "src/**/*.json", ".env"],
  "exclude": ["node_modules"]
}

Note: with an existing tsconfig setup, it is likely only the "emitDecoratorMetadata" (emit metadata for decorators) and "experimentalDecorators" (turns on ES7 decorators) need adding.

Now add a nodemon.json file:

{
  "watch": ["src", ".env"],
  "ext": "js,ts,json",
  "exec": "ts-node -r tsconfig-paths/register --transpile-only src/server.ts"
}

Now we have the basics set up for what we are going to need to use.

Setting up the express server to use Routing Controllers

Create a file ./src/server.ts:

// src/server.ts
import { createExpressServer } from 'routing-controllers';

const PORT = 4000;

console.info(`Starting server on http://localhost:${PORT}`);

const routes = []; // To be changed soon

const app = createExpressServer(
    {
        controllers: routes,
    }
);

app.listen(PORT);

Now if you run yarn dev and you visit localhost:4000 it should respond (with an error - soon to fix).

Create your first controller

So now we have the web server, running Routing Controllers. We just don't have any controllers.

Create a file ./src/controllers/HelloWorldController.ts:

// HelloWorldController.ts
import { Controller, Get, QueryParam } from 'routing-controllers';
import 'reflect-metadata';

@Controller('/hello-world')
export class HelloWorldController {
  @Get('/')
  index() {
    return "hello world!";
  }
}

And then update server.ts. First import this class:

import HelloWorldController from './controllers/HelloWorldController.ts';

and use that in the routes array:

const routes = [ HelloWorldController ]; // we will be adding more here soon.

Now if you visit http://localhost:4000/hello-world, you should see it respond with "hello world".

Access URL query params

Let's say now we want to be able to visit http://localhost:4000/hello-world?goodbye=true, and we want it to respond with 'goodbye'.

Update the HelloWorldController class to this:

// HelloWorldController.ts
import { Controller, Get, QueryParam } from 'routing-controllers';
import 'reflect-metadata';

@Controller('/hello-world')
export class HelloWorldController {
  // this will look for /hello-world?goodbye=true
  @Get('/')
  index(@QueryParam('goodbye') goodbye: string) {
    if(goodbye === 'true') return 'goodbye';
    return "hello world!";
  }
}

Note: the goodbye param is a string ('true').

Getting params from the route (URL)

Let's create a new method in HelloWorldController that shows a 'post', which has a 'post id' param. We want to be able to visit localhost/hello-world/post/123, and get 123 as a variable.

In our example we are hard coding what to do with the post ID, but in a real app you would probably request it from a db.

// this will look for /123 (and set '123' as postId)
@Get('/post/:id')
show(@Param('id') postId: string) {
  return `Showing post ${postId}`
  // (real app would return a blog post data)
}

Accept POST params

Let's say we have a FE web app that does the following request:

window.fetch(
  'http://localhost:4000/hello-world/add',
   {
     method: "POST",
     body: JSON.stringify({message: "hello"})
    }
   )
   .then(res => res.json())
   .then(console.log)

So we are expecting our BE server to accept a POST request on /hello-world/add, and it is expecting the POST body to be JSON data with a message.

First, we need to set up CORS on the BE.

yarn add cors

and then update server.ts to this (adding the cors config)

// src/server.ts
import { createExpressServer } from 'routing-controllers';
import { HelloWorldController } from './controllers/HelloWorldController';

const PORT = 4000;

console.info(`Starting server on http://localhost:${PORT}`);

const routes = [HelloWorldController];

const app = createExpressServer(
    {
        controllers: routes,
        cors: {
            origin: '*', // (note: do not use this in production)
        }
    }
);

app.listen(PORT);

Note: on a real app you should not be adding origin: '*'. If your FE is running on localhost:3000, then for a real app you should replace the '*' with 'locahost:3000'. This is not a tutorial on CORS though so we are fine with adding a wildcard here.

Now we need to add the POST request to /hello-world/add handler.

Update HelloWorldController.ts first by importing Body:

// HelloWorldController.ts
import { Body, Controller, Get, Param, Post, QueryParam }
    from 'routing-controllers';

add this method:

// ...
@Post('/add')
store(@Body() message: any) {
  return `request had body:
          "${JSON.stringify(message)}"`
}

One nice thing we can do is pass in a class as the type, and Routing Controllers will use that when injecting in the body.

Above your class definition, add this (in a real app - this should be in a separate file):

class HelloWorldPost {
    message: string
}

And update your store method to this:

@Post('/add')
store(@Body() newPostObj: HelloWorldPost) {
  return {
      type: typeof newPostObj,
      isHelloWorldPost: newPostObj.constructor.name,
      body: newPostObj,
  }
}

Now when you post data to it, this is the output, showing what the newPostObj is:

{
    "type": "object",
    "isHelloWorldPost": "HelloWorldPost",
    "body": {
        "message": "Hello"
    }
}

You can see the newPostObj is an instance of HelloWorldPost.

At the moment it seems quite basic, but this is very useful (I see it quite similar to using 'Form Requests' in Laravel) - especially when you start adding validation.

Here is an example of adding validation:

export class User {
  @IsEmail()
  email: string;

  @MinLength(6)
  password: string;
}

This uses the https://github.com/typestack/class-validator package - have a look there to see a list of what validators you can call. Here are some useful and commonly used ones:

@IsEmail()
@IsJSON()
@IsJWT()
@ArrayContains(values: any[])

And here are some more obscure ones, to give you an idea of what is available:

@IsIBAN() // Checks if a string is a IBAN
          // (International Bank Account Number).
@IsLatitude() // Checks if the string or number
              // is a valid latitude coordinate.
@IsMimeType() // Checks if the string matches to
              // a valid MIME type format

Returning JSON from your controller

At the moment we've been returning strings, and they're returned as strings (as you would expect). You could return an array or object and it would return a JSON object (as you could probably guess).

But maybe you want to force the controller to always use JSON...

Change the controller setup from this:

@Controller('/hello-world') // << change this
export class HelloWorldController {
    // ...
}

You can simply change @Controller to:

@JSONController('/hello-world') // << to this
export class HelloWorldController {
    // ...
}

Doing this will:

  • transform any returned data into JSON.
  • the response Content-Type header will be set to application/json
  • requests with the content-type header application/json will parse the body as JSON.

Set HTTP status code

In this example it will return a 201 (created) http status code:

@HttpCode(201)
@Post("/add")
store(@Body() someObject: any) {
    // ...
}

Get the Express request/response variables

If you're used to working with Express, then you probably have response handlers that take two args: req, res (request, response)

You can inject those in as arguments to your controller methods with @Request() and @Response().

@Get('/')
helloWorld(@Req() request: any, @Res() response: any) {
    return response.send('some response...');
}

Return a redirect http header location:

@Get('/')
helloWorld(@Res() response: any) {
    return response.redirect('/hello-world-redirect');
}

You can also use a decorator to redirect:

@Get('/')
@Redirect('/hello-world-redirect')
helloWorld() {
    // ...
}

... or you can override the URL in @Redirect() by returning a URL:

@Get('/')
@Redirect('/hello-world-redirect')
helloWorld() {
    return '/actually-redirect-to-this';
}

Inject header params in your controller methods

@Get("/hello-world")
index(@HeaderParam("authorization") token: string) {
    return `Your HTTP header's auth token: ${token}`
}

Get cookies:

You can also inject a single cookie with the @CookieParam():

@Get("/hello-world")
index(@CookieParam("some-cookie-param") cookieParam: string) {
    return `Your cookie param was ${cookieParam}`
}

You can get all cookies with @CookieParams()

Automatically include all controllers

At the moment our config looks like this:

// ...
const routes = [HelloWorldController];
// ...
const app = createExpressServer(
    {
        controllers: routes,
        // ...
    }
)

Every time we add a new controller we would need to update this.

Depending on your needs, you might prefer to use a glob like this:

const app = createExpressServer(
    {
        controllers: [__dirname + '/controllers/*.ts']
        // ...
    }
)

More on Routing Controllers coming soon.

Comments Introduction to Routing-Controllers