Table of contents
- Installation guide for routing-controllers (with express)
- Setting up the express server to use Routing Controllers
- Create your first controller
- Access URL query params
- Getting params from the route (URL)
- Accept POST params
- Returning JSON from your controller
- Set HTTP status code
- Get the
Express
request/response variables - Inject header params in your controller methods
- Get cookies:
- Automatically include all controllers
- Next steps
Updated March 2024
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 guide for routing-controllers (with express)
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;
yarn add -D @types/express @types/body-parser @types/multer @types/node nodemon cross-env tsconfig-paths tslib;
- we need
reflect metadata
, as a poly fill for the metadata proposal - https://rbuckton.github.io/reflect-metadata/ ) body-parser
to parse incoming HTTP request body (we will use this with POST requests later)- Other imports are used here (but not referenced elsewhere in this guide, such as
tslib
, as they are required by other dependencies to run this routing-controllers app.
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 add a tsconfig.json
and add the following to it:
{
"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: If you are adding routing-controllers to an existing express app and you already have a tsconfig file, then you will probably just need to add the "emitDecoratorMetadata"
(emit metadata for decorators) and "experimentalDecorators"
(turns on ES7 decorators) configs.
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') // << the base url for everything in this controller, e.g. localhost:4000/hello-world
export class HelloWorldController {
@Get('/') /* << sub path, so this will be `localhost:4000/hello-world, but you could have
@Get('/page2') for a endpoint on localhost:4000/hello-world/page2 */
index() {
// whatever you return here will be sent as the response. In this case
// we can even include html...
return "hello world from <a href=https://webdevetc.com/>webdevetc</a>!";
}
}
And then update server.ts
. First import this class:
import {HelloWorldController} from './controllers/HelloWorldController';
and use that in the routes
array (also in server.ts
):
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 without understanding implications)
}
}
);
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 toapplication/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) {
// ...
}
Express
request/response variables
Get the 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.
Next steps
I also have a blog post on setting up mongodb with routing-controllers
Comments →Introduction to Routing-Controllers