Controllers
The controllers layer is responsible for handling incoming requests, and return a response to the client.
To create a basic controller you have to attach the metadata to the class. Thanks to the metadata Nest knows how to map your controller into the appropriate routes. To attach the metadata we're using the decorators (in this case @Controller('cats')
).
1 2 3 4 5 6 7 8 9 | import { Controller, Get } from '@nestjs/common'; @Controller('cats') export class CatsController { @Get() findAll() { return []; } } |
Metadata
We're using @Controller('cats')
here. This decorator is obligatory. The cats
is a prefix for each route registered in the class. The prefix is optional what means that you could leave the parentheses empty (@Controller()
), but it reduces redundant boilerplate code, thus you don't have to repeat yourself every time when you'd decide to create a new endpoint (route).
There's a single public method, the findAll()
, which returns an empty array. The @Get()
decorator tells Nest that it's necessary to create an endpoint for this route path and map every appropriate request to this handler. Since we declared the prefix for every route (cats
), Nest will map every /cats
GET request here.
When a client would call this endpoint, Nest will return with 200 status code, and the parsed JSON, so in this case - just an empty array. How is that possible?
There are two possible approaches of manipulating the response:
Method | Description |
---|---|
Standard (recommended) | We're treating the handlers in the same way as plain functions. When we return the JavaScript object or array, it'd be automatically transformed to JSON. When we return the string, Nest will send just a string. Furthermore, the response status code is always 200 by default, except POST requests, when it's 201. We can easily change this behavior by adding the @HttpCode(...) decorator at a handler-level. |
Express | We can use the express response, which we can inject using @Res() decorator in the function signature, for example findAll(@Res() response) . |
Warning
It's forbidden to use both two approaches at the same time. Nest detects whether the handler is using @Res()
or @Next()
, and if it's truth - the standard way is disabled for this single route.
Request Object
A lot of endpoints need an access to the client request details. In fact, Nest is using express request object. We can force Nest to inject the request object into handler using @Req()
decorator.
Hint
There's a @types/express
package and we strongly recommend to use it (Request
has its own typings).
TypeScript
1 2 3 4 5 6 7 8 9 | import { Controller, Get, Req } from '@nestjs/common'; @Controller('cats') export class CatsController { @Get() findAll(@Req() request) { return []; } } |
JavaScript
1 2 3 4 5 6 7 8 9 10 | import { Controller, Bind, Get, Req } from '@nestjs/common'; @Controller('cats') export class CatsController { @Get() @Bind(Req()) findAll(request) { return []; } } |
The request object represents the HTTP request and has properties for the request query string, parameters, HTTP headers, and e.g. body (read more here), but in most cases, it's not necessary to grab them manually.
We can use dedicated decorators instead, such as @Body()
or @Query()
, which are available out of the box.
Below is a comparison of the decorators with the plain express objects.
Decorator | Express Equivalent |
---|---|
@Request() |
req |
@Response() |
res |
@Next() |
next |
@Session() |
req.session |
@Param(param?: string) |
req.params / req.params[param] |
@Body(param?: string) |
req.body / req.body[param] |
@Query(param?: string) |
req.query / req.query[param] |
@Headers(param?: string) |
req.headers / req.headers[param] |
More Endpoints
We have already created an endpoint to fetch the data (GET route). It'd be great to provide a way of creating the new records too. Let's create the POST handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import { Controller, Get, Post } from '@nestjs/common'; @Controller('cats') export class CatsController { @Post() create() { // TODO: Add some logic here } @Get() findAll() { return []; } } |
It's really easy. Nest provides the rest of those endpoints decorators in the same fashion - @Put()
, @Delete()
, @Patch()
, @Options()
, @Head()
, and @All()
.
Status Code Manipulation
As mentioned, the response status code is always 200 by default, except POST requests, when it's 201. We can easily change this behavior by adding the @HttpCode(...)
decorator at a handler-level.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import { Controller, Get, Post, HttpStatus } from '@nestjs/common'; @Controller('cats') export class CatsController { @HttpStatus(204) @Post() create() { // TODO: Add some logic here } @Get() findAll() { return []; } } |
Route parameters
Routes with static paths can't help when you need to accept dynamic data as part of the URL. To define routes with route parameters, simply specify the route parameters in the path of the route as shown below.
1 2 3 4 5 | @Get(':id') findOne(@Param() params) { console.log(params.id); return {}; } |
Async / Await
We love modern JavaScript, and we know that the data extraction is mostly asynchronous. That's why Nest supports async
functions, and works pretty well with them.
Hint
Learn more about async / await
here!
Every async function has to return the Promise
. It means that you can return deffered value and Nest will resolve it by itself. Let's have a look on the below example:
TypeScript
1 2 3 4 | @Get() async findAll(): Promise<any[]> { return []; } |
JavaScript
1 2 3 4 | @Get() async findAll() { return []; } |
Observables
Furthermore, Nest route handlers are even more powerful. They can return the RxJS observable streams. It makes the migration between a simple web application and the Nest microservice much easier.
TypeScript
1 2 3 4 | @Get() findAll(): Observable<any[]> { return Observable.of([]); } |
JavaScript
1 2 3 4 | @Get() findAll() { return Observable.of([]); } |
There is no "best practice" in this regard; use whatever format suits your needs.
POST handler
That's strange that this POST route handler doesn't accept any client params. We should at least expect the @Body()
argument here.
Firstly, we need to establish the DTO (Data Transfer Object) schema. A DTO is an object that defines how the data will be sent over the network. We could do it using TypeScript interfaces, or by simple classes. What may be surprising, we recommend using classes here. Why? The classes are the part of the JavaScript ES6 standard, so they're just plain functions. On the other hand, TypeScript interfaces are removed during the transpilation, Nest can't refer to them. It's important because features such as Pipes enables additional possibilities when they've access to the metatype of the variable.
Let's create the CreateCatDto
:
1 2 3 4 5 | export class CreateCatDto { readonly name: string; readonly age: number; readonly breed: string; } |
It has only three basic properties. All of them are marked as a readonly
, because we should always try to make our functions as pure as possible.
Now we can use the schema inside the CatsController
:
TypeScript
1 2 3 4 | @Post() async create(@Body() createCatDto: CreateCatDto) { // TODO: Add some logic here } |
JavaScript
1 2 3 4 5 | @Post() @Bind(Body()) async create(createCatDto) { // TODO: Add some logic here } |
Error Handling
There's a separated chapter about working with the exceptions here.
Last Step
The controller is prepared, and ready to use, but Nest doesn't know that CatsController
exists yet, so it won't create an instance of this class. We need to tell about it.
The controller always belongs to the module, that's why we hold controllers
array within @Module()
decorator. Since we don't have any other modules except the root ApplicationModule
, let's use it for now:
1 2 3 4 5 6 7 | import { Module } from '@nestjs/common'; import { CatsController } from './cats/cats.controller'; @Module({ controllers: [CatsController], }) export class ApplicationModule { } |
Tada! We attached the metadata to the module class, so now Nest can easily reflect which controllers have to be mounted.
Express Approach
The second way of manipulating the response is to use express response object. It was the only available option until Nest 4. To inject the response object, we need to use @Res()
decorator. To show the differences, i'm going to rewrite the CatsController
:
TypeScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import { Controller, Get, Post, Res, Body, HttpStatus } from '@nestjs/common'; import { CreateCatDto } from './dto/create-cat.dto'; @Controller('cats') export class CatsController { @Post() create(@Res() res, @Body() createCatDto: CreateCatDto) { // TODO: Add some logic here res.status(HttpStatus.CREATED).send(); } @Get() findAll(@Res() res) { res.status(HttpStatus.OK).json([]); } } |
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import { Controller, Get, Post, Bind, Res, Body, HttpStatus } from '@nestjs/common'; @Controller('cats') export class CatsController { @Post() @Bind(Res(), Body()) create(res, createCatDto) { // TODO: Add some logic here res.status(HttpStatus.CREATED).send(); } @Get() @Bind(Res()) findAll(res) { res.status(HttpStatus.OK).json([]); } } |
This manner is much less clear from my point of view. I definitely prefer the first approach, but to make the Nest backward compatible with the previous versions, this method is still available. Also, the response object gives more flexibility - you've full control of the response.