Node.js Framework Series — 1.2.1. NestJS — Controllers
Controllers are the most important as a building block. They are the responsible for handling incoming requests and returning responses to the client. We are able to receive certain requests to the application, doing it with the routing mechanism. The routing mechanism manages which controller receives which requests with the following decorators:
As you see on the diagram, each controller can have more than one route, and each route can perform different jobs.
As a best practice, better to create one controller for each component. So, we will have many components and also controllers and each controller will have many routers, and routers will handle our RestAPIs’ methods such as GET, PUT, POST, DELETE and so on with decorators, and all of the routers will handle our incoming requests and outgoing responses.
To create a controller, always keep in your mind that ‘loosely coupled[1] and high cohesion[2]’ principles. Keep all of the related incoming requests in a same controller.
In general, basic controllers consist of the following items:
- Routing
- Request object
- Resources
- Route wildcards
- Status code
- Headers
- Redirection
- Route parameters
- Sub-domain Routing
- Scopes
- Asynchronicity
- Request payloads
Let’s explain them with one simple RestAPIs microservice. Let say we will create a website. Let’s create high level possible microservice diagram as follows:
Let’s use Property Users microservice as a sample. Head back to the terminal and create a service as follows:
$ nest new property-users # creates NestJs project
PS, to create a controller, run the following commands:
$ nest g co {controller-name}
And for our project, go inside the folder, create a controller with the following command:
$ cd property-users$ nest g co login # creates login controller class$ nest g co register # creates register controller class$ nest g co profile # creates profile controller class$ cd src$ tree # creates folder tree
After above commands, our project under src/ folder will be as follows:
.├── app.controller.spec.ts├── app.controller.ts├── app.module.ts├── app.service.ts├── login│ ├── login.controller.spec.ts│ └── login.controller.ts├── main.ts├── profile│ ├── profile.controller.spec.ts│ └── profile.controller.ts└── register├── register.controller.spec.ts└── register.controller.ts
Now, we are ready to look at NestJS Controller’s items, let’s start with routers.
Routing
In the following example we have a class as a basic controller which is done by @Controller decorator. @Controller decorator can take a parameter as a prefix for a route/endpoint path, for example ProfileController’s decorator takes “profile” as follows. Therefore, we need to call this class’s HTTP methods with {url}/profile/route-method. Otherwise, it won’t be possible to reach appropriate HTTP method. We are able to group our routes with controller decorator’s path prefix, so no need to repeat.
profile.controller.ts
import { Controller, Get } from '@nestjs/common';@Controller('profile')
export class ProfileController {
@Get('all')
findAllUser(): string {
return 'This endpoint returns all registered users';
}
}
As you see above, we have one route with @Get() decorator before the findAllUser() in there. This decorator tells NestJS to create a mapping between a specific endpoint and method(findAllUser). It makes it with route path. The route path is determined by concatenating the controller’s path prefix and any path specified in the request decorator. For example in our example the endpoint will be http://localhost:3000/profile/all, so pattern is {root_url}:{port}/{controller_prefix}/{router_path}. But NestJS provides decorators for all of the standard HTTP Methods[3] for routing as follows:
- A @Get() decorator : Routes HTTP GET requests to specified path
- A @Post() decorator: Routes HTTP POST requests to specified path
- A @Put()decorator: Routes HTTP PUT requests to specified path
- A @Delete() decorator: Routes HTTP DELETE requests to specified path
- A @Option() decorator: Routes HTTP OPTION requests to specified path
- A @Patch() decorator: Routes HTTP PATCH requests to specified path
- A @Head() decorator: Routes HTTP HEAD requests to specified path
- A @All() decorator: Routes all HTTP requests to specified path
PS, in this example above, when we made a GET request, NestJS routes the request to findUserAll method, in fact, the method name below the decorators are arbitrary. So method name after route decorator is not important.
The findUserAll method returns by default 200 on the header with the associated response. The response status is always 200 for the HTTP methods except POST method which use 201. You can also change it with @HttpCode(…) decorator. NestJS provides two different options for responses:
- Standart(recommended by NestJS): If your response object returns JS object or array, it will automatically be serialised to JSON. If your response returns primitive type(e.g., string, number, boolean), NestJS will return value without serialising. In this method, no need to worry about the response, just return the value and NestJS take care of your response.
- Library-specific: For example, we can use library-specific response by using @Res() decorator like below for the Express, we need to handle all of the header status in this method:
profile.controller.ts
import { Controller, Get, Res } from '@nestjs/common';
import { response } from 'express';@Controller('profile')
export class ProfileController {
@Get('all')
findAllUser(@Res() reponse) {
response.status(200).send('This endpoint returns all registered users');
}
}
PS, we cannot use both method at the same time.
Request Object
When we need to access the the client request details, NestJS provides access to the actual request object. For example, we can reach the request object by using @Req() decorator as follows:
profile.controller.ts
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';@Controller('profile')
export class ProfileController {
@Get('all')
findAllUser(@Res() response: Response) {
return 'This endpoint returns all registered users';
}
}
PS, if you want to use express features in your NestJS application, you need install @type/express package.
The request object represent the HTTP request and it’s properties. But instead of using Express(by default) objects, you can use NestJS dedicated decorators as follows:
- @Request(), @Req() : req
- @Response(), @Res(): res
- @Next(): next
- @Session(): req.session
- @Param(key?: string): req.params / req.params[key]
- @Body(key?: string): req.body / req.body[key]
- @Query(key?: string): req.query / req.query[key]
- @Headers(name?: string): req.headers / req.headers[name]
- @Ip(): req.ip
- @HostParam(): req.hosts
Resources
In a routing section, we learnt that how we can get all data with GET method. For example if we want to create new records, we are doing it with POST method by using @Post decorator as follows:
register.controller.ts
import { Controller, Post } from '@nestjs/common';@Controller('register')
export class RegisterController {
@Post()
register() {
return 'This method adds a new user';
}
}
As we mentioned in the routing section, NestJS provides decorators for HTTP methods: @Get, @Post, @Put, @Delete, @Patch, @Options and @Heads, also @All for handling the all of them.
Route wildcards
To handling a pattern based routes, we can use the following:
profile.controller.ts
import { Controller, Get, Res } from '@nestjs/common';@Controller('profile')
export class ProfileController {
@Get('ab*c')
findAll() {
return 'This endpoint returns all registered users';
}
}
For example, for the localhost, the ‘ab*cd’ route will match and handle the following endpoints:
- http://localhost:3000/profile/abxcd
- http://localhost:3000/profile/ab1cd
- http://localhost:3000/profile/ab_cd
- http://localhost:3000/profile/ab9cd
As you see above, we are able to use regular expression for the routes, too.
Status code
We can customize our response header status as follows:
register.controller.ts
import { Controller, HttpCode, Post } from '@nestjs/common';@Controller('register')
export class RegisterController {
@Post()
@HttpCode(203)
register() {
return 'This method adds a new user';
}
}
Header
For example when we need no-cache for our endpoints, we can customize our response header as follows:
register.controller.ts
import { Controller, HttpCode, Header, Post } from '@nestjs/common';@Controller('register')
export class RegisterController {
@Post()
@Header('Cache-Control', 'none')
@HttpCode(203)
register() {
return 'This method adds a new user';
}
}
Redirection
Sometimes you need to redirect your response to another RestAPI, you can use NestJS redirection decorator as follows:
register.controller.ts
import { Controller, HttpCode, Header, Redirect, Post } from '@nestjs/common';@Controller('register')
export class RegisterController {
@Post()
@Redirect('https://localhost:3000/login', 301)
register() {
return 'This method adds a new user';
}
}
You can also determine the HTTP status code or redirect URL dynamically. You can do it by returning an object like below:
{
"url": string,
"statusCode": number
}
Returned values override @Redirect() decorator, let’s see it with an example as follows:
register.controller.ts
import { Controller, Redirect, Post, Query } from '@nestjs/common';@Controller('register')
export class RegisterController {
@Post()
@Redirect('https://localhost:3000/login', 301)
register(@Query('version') version: string) {
if (version && version === '2') {
return { url: 'https://localhost:3000/v2/login' };
} return 'This method adds a new user';
}
}
Route parameters
We can get parameters from the route path as well. To do it, easily we can define a variable handler with colon(:), for example @Get(‘propery/:propertyId’).
@Get(':userId')
getUserDetails(@Param() params) {
console.log(params.userId);
return `User id is #${params.userId}`;
}
As you see on the above example, we have a variable identifier on the GET decorator, and we have another decorator to pick up this parameter on the method by using @Param() decorator. param keeps all of the variable as a JSON object. If you want to pick up only userId from the path, do the follows:
profile.controller.ts
import { Controller, Get, Param } from '@nestjs/common';@Controller('profile')
export class ProfileController {
@Get(':userId')
getUserDetails(@Param('userId') id) {
console.log(id);
return `User id is #${id} `;
}
}
Sub-domain Routing
You can manage your subdomain with NestJS decorator. @Controller decorator can take a host option to validate the HTTP host of the incoming requests as follows :
@Controller({ host: 'admin.zero2hero-property.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
Host option can take a dynamic parameter as follows:
@Controller({ host: ':client.zero2hero-property.com' })
export class AccountController {
@Get()
getInfo(@HostParam('client') client: string) {
return client;
}
}
Scopes
Another important topic is scopes. As you know, node.js by default is single thread and asynchronously runs our events. We share almost everything across incoming requests. We have a connection pool to the database, singleton services with global state and so on. Just because of Node.js nature, using singleton instances is fully safe for our application.
Time to time we need more than singleton scope for our application, for instance per-request caching in GraphQL apps, request tracking and multi-tenancy apps, when we need request-based lifetime, we need more scopes. NestJS provides 3 different type of scopes as follows:
- DEFAULT scope: we can say it ‘singleton scope’. A single instance of the provider is shared across the entire application. The instance lifetime is tied directly to the application lifecycle. when the app is started, all singleton providers have been instantiated. Singleton scope is used by default.
- REQUEST scope: A new instance of the provider is created exclusively for every incoming request. When the request is completed processing, the instance is garbage-collected.
- TRANSIENT scope: Transient providers are not shared across consumers. Each consumer that injects a transient provider will receive a new, dedicated instance.
Asynchronicity
NestJS support async/await so well. When we use async function, we need to return a Promise. It means that you can return deferred value that NestJS will be able to handle to resolve by itself.
profile.controller.ts
import { Controller, Get, Param } from '@nestjs/common';@Controller('profile')
export class ProfileController {
@Get(':userId')
async getUserDetails(@Param('userId') id: number): Promise<any[]> {
console.log(id);
return new Promise(resolve => {
resolve([id]);
});
}
}
Request Payloads
For example, in our property website example, we need to register users and also sellers. To do so, we need to pick up all of the required parameters for the registration. We do such things via DTO(Data Transfer Object) schema. A DTO is an object that contains all of the variable with type definitions. We could determine the DTO schema by using TypeScript interface or by simple JavaScript classes. Better to use JS classes here. Classes are part of the JavaScript ES6 standard, and therefore they are preserved as real entities in the compiled JavaScript. Let see them on the following example:
Let’s create CreateRegisterDto class:
register.dto.ts
export class RegisterDto {
username: string;
password: string;
firstName: string;
lastName: string;
dob: Date;
address: string;
postCode: string;
city: string;
state: string;
country: string;
phone: string;
email: string;
}
We defined all of the required parameters in this DTO. And we will use it in our controller class. NestJS has another decorator to map all of the incoming POST variables to DTO as a @Body() decorator. We can use it as follows:
register.controller.ts
import { Controller, Body, Post } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';@Controller('register')
export class RegisterController {
@Post()
register(@Body() register: RegisterDto) {
console.log(`Register parameters: ${JSON.stringify(register)}`); return 'This method adds a new user';
}
}
Controllers were the important topics, especially if you use NestJS for the Rest APIs development. Let’s create a possible controller with mostly used decorators which exposes a couple of methods and routes to access and manipulate our internal data:
profile.controller.ts
@Controller('profile')
export class ProfileController {
@Post()
create(@Body() createProfileDto: CreateProfileDto) {
return 'This endpoint adds a new cat';
}@Get()
findAll(@Query() query: ListAllEntities) {
return `This endpoint returns all users (limit: ${query.limit} items)`;
}@Get(':id')
findOne(@Param('id') id: number) {
return `This endpoint returns a #${id} user`;
}@Get(':userName')
async getUserDetails(@Param('userName') userName: string) {
return 'returns user details';
}@Put(':id')
update(@Param('id') id: string, @Body() updateProfileDto: UpdateProfileDto) {
return `This endpoint updates a #${id} user`;
}@Delete(':id')
remove(@Param('id') id: string) {
return `This endpoint removes a #${id} user`;
}
}
Getting up and running
When we implement all of routes and our controller is ready to use, NestJS still doesn’t know what ProfileController is, and don’t know that ProfileController exists. Therefore, NestJS won’t return a result and won’t create an instance of controller class.
So, controllers belong to a module. We need to define all of our controller in a module class or within the @Module() decorator as follows:
app.module.ts
import { Module } from '@nestjs/common';import { AppController } from './app.controller';import { AppService } from './app.service';import { LoginController } from './login/login.controller';import { RegisterController } from './register/register.controller';import { ProfileController } from './profile/profile.controller';@Module({imports: [],controllers: [AppController, LoginController, RegisterController, ProfileController],providers: [AppService],})export class AppModule {}
Resource:
[1] Coupling, 2020, available at https://en.wikipedia.org/wiki/Coupling_(computer_programming)
[3] HTTP request methods, 2020, available at https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods