NestJS is a Node.js web framework built with TypeScript used to build scalable server-side applications. The server side of an application, which does not have UI elements, performs important logic like handling requests, sending responses, storing data in a database, and much more.
Controllers and routes play an important role in server-side applications. In this article, we’ll take a look at routes and controllers in NestJS. We will cover:
To follow along, you’ll need to install Node.js, a JavaScript package manager — I’m using Yarn — and an IDE or text editor of your choice, like Sublime Text or Visual Studio Code. Make sure you’re using Node v12 or newer.
A server-side, or HTTP, request is what we refer to as a user asking the application to perform an action related to a resource. These user requests are made through a client, such as a browser or application.
When we ask our application to perform certain actions, these HTTP requests are classified by verbs. Here are a few commonly used verbs and what requests they relate to:
GET /users
GET /users/A
*POST /users*
PUT /users/A
DELETE /users/A
You may have noticed that in the examples above, we referred to our resource in the plural users
. There is no right or wrong way to name your resources, but this is a popular convention for representing all your resources. For a specific resource, you would follow the plural resource name with a unique identifier — for example, users/A
or users/1
.
These are the most frequently used HTTP verbs, but there are a few others, such as PATCH, OPTIONS, and more. You can read about all the HTTP verbs and methods in the MDN docs.
Now that we have reviewed HTTP requests, let’s talk about how routes and controllers are used in Nest.
A route is a combination of an HTTP method, a path, and the function to handle it defined in an application. They help determine which controllers receive certain requests.
A controller is a class defined with methods for handling one or more requests. The controller provides the handler function for a route.
Note that in NestJS, the routes are defined in the controller alongside the handler method using decorators.
Learn more about handling requests and responses with controllers and routes in Nest.
We will create our project using the NestJS CLI. Let’s install it now:
$ npm i -g @nestjs/cli
Once that’s installed, we can run the Nest command to create a new project:
$ nest new fruit-tree
In this case, fruit-tree
is our project name. This is a simple project that returns Tree
metadata and illustrates how routes work in NestJS.
Follow the prompts given after running the Nest command. Nest will ask what you’d like to use as your package manager. I chose Yarn. And that’s it, our project is ready!
Nest creates some core files in the src/
directory of our project. Let’s go through these files:
app.controller.ts
is a basic controller class with a single route definitionapp.controller.spec.ts
is the unit test file for the controller class app.controller.ts
app.module.ts
this is the root module for our application. All other modules we create will be registered in hereapp.service.ts
is a basic service used by our controller to perform some logicmain.ts
is the entry point of our applicationThe main.ts
file is where the Nest application instance is created and registered to a port for access from our client apps.
Let’s start creating our own resources, routes, and controllers in our Nest project.
Let’s set up a MySQL database and ORM for retrieving and updating data. First, we will install @nestjs/typeform
, typeform
, and mysql2
by running the following command:
$ yarn add @nestjs/typeorm typeorm mysql2
Now that our needed libraries are installed, let’s import and register the TypeOrmModule
in our core module AppModule
:
// app.module.ts import { TypeOrmModule } from '@nestjs/typeorm'; import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'xxxx', database: 'fruit-tree', entities: [], synchronize: true, }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
Once TypeOrmModule
is registered in AppModule
, we will not need to register it in any other modules we create. Note that you may want to set up a config like Dotenv so you don’t commit your MySQL credentials.
After registering the TypeOrmModule
in our core module, we can inject DataSource
and EntityManager
objects from TypeOrm in other modules of our project.
Tree
entity and TreeService
TypeORM uses the repository design pattern, which is a design paradigm that allows you to abstract communication with the data layer in a way that separates business logic from data objects and the type of database used to manage them.
To start, we’ll create a tree
directory for our tree module. This directory will house the tree entity and, eventually, the routes, controllers, and services for tree requests:
$ cd src/ $ mkdir tree $ cd tree/ $ touch tree.entity.ts
In our tree.entity.ts
, we will specify a few simple columns and attributes:
// tree.entity.ts import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Tree { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() age: number; @Column() isEndangered: boolean; }
Let’s create our TreeService
for running operations on our Tree
entity:
$ touch tree.service.ts
We will inject our treeRepository
in our TreeService
to perform operations on the entity:
// tree.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Tree } from './tree.entity'; @Injectable() export class TreeService { constructor( @InjectRepository(Tree) private treeRepository: Repository<Tree>, ) {} findAll(): Promise<Tree[]> { return this.treeRepository.find(); } findOne(id: number): Promise<Tree> { return this.treeRepository.findOneBy({ id }); } async deleteById(id: number): Promise<void> { await this.treeRepository.delete(id); } }
Now we’ll create our TreeModule
and register our Tree
entity and TreeService
:
$ touch tree.module.ts // tree.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Tree } from './tree.entity'; import { TreeService } from './tree.service'; @Module({ imports: [TypeOrmModule.forFeature([Tree])], providers: [TreeService], }) export class TreeModule {}
We will also register the TreeModule
in the core module in app.module.ts
:
// app.module.ts ... import { TreeModule } from './tree/tree.module'; @Module({ imports: [ ... TreeModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
So far in our project, we’ve set up a MySQL database, TypeORM to handle operations to our DB, a TreeEntity
class where we have defined the attributes of our data entity, and a service through which we can read or write into our data entities.
Now we will create some Nest routes, along with controller methods to handle those routes! Remember: a route is a medium through which your user can request your application to perform certain operations, while the controller is the class through which we honor those route requests.
Let’s define a route to fetch all trees in our database. First, we’ll create our controller file:
$ cd src/trees/ $ touch tree.controller.ts
In our tree.controller.ts
file, we’ll import the needed decorators from @nestjs/common
. We will then define our route using the intended HTTP verb decorator along with the controller method to handle that route:
// tree.controller.ts import { Controller, Get } from '@nestjs/common'; import { Tree } from './tree.entity'; import { TreeService } from './tree.service'; @Controller('trees') export class TreeController { constructor(private readonly treeService: TreeService) {} @Get() getTrees(): Promise<Tree[]> { return this.treeService.findAll(); } }
Note that in the @Controller
decorator, we set the path prefix for all the routes that will be defined in this file. For example, the route defined in our above file is GET /trees
.
Our controller method calls the findAll()
method of the treeService
to return a list of our tree entities.
Don’t forget to register the controller in the module:
// tree.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TreeController } from './tree.controller'; import { Tree } from './tree.entity'; import { TreeService } from './tree.service'; @Module({ imports: [TypeOrmModule.forFeature([Tree])], providers: [TreeService], controllers: [TreeController], }) export class TreeModule {}
We defined a very simple route above to fetch and return a list of resources. What if we want to define a route on a specific resource item — for example, to retrieve a single item using its unique identifier? Let’s take a look!
In our controller — in this case, tree.controller.ts
— we’ll import an additional decorator from @nestjs/common
called Param
and use it in our controller as shown below:
// tree.controller.ts import { Controller, Get, Param, Req, Res } from '@nestjs/common'; import { Tree } from './tree.entity'; import { TreeService } from './tree.service'; @Controller('trees') export class TreeController { constructor(private readonly treeService: TreeService) {} @Get() getTrees(): Promise<Tree[]> { return this.treeService.findAll(); } @Get('/:id') getSingleTree(@Param() params: { id: number }): Promise<Tree> { return this.treeService.findOne(params.id); } }
​​In the snippet above, we defined a route with the decorator @Get('/:id')
​ to signify that a parameter id​
will be passed in. To access the parameter id​
inside our handler controller method, we passed the @Param()​
decorator as a parameter for our method.
To actually use the content of @Param()
,​ we assigned it a variable name params
​. Additionally, because it’s TypeScript, we defined the contents of params​
as an interface { id: number }
​.
Now in the method, we can reference the ID using params.id
. The route we’ve defined will match GET /trees/:id
.
Note that in our example above, we passed in and referenced the entire param
object. Another option is to only pass in the id
parameter we need to the controller method. We can do this by using @Param('id') id: string
and referencing just id
in our controller method.
We’ve defined simple routes with no parameters; we’ve defined routes with parameters. But how do we handle routes that accept a payload — for example, to create an entity with that payload? Let’s take a look.
For this example, we will define a POST
route. The first step is to add Post
to our import list from @nestjs/common
. We’ll also import Body
from @nestjs/common
, through which we’ll pass the payload body to the controller method for our route.
First, we’re going to do a minor detour to keep things clean and define a DTO class. This class will define the attributes we expect in our payload body:
$ cd src/trees $ mkdir dto $ cd dto/ $ touch create-tree.dto.ts
In our DTO file, we need to add the following:
// create-tree.dto.ts export class CreateTreeDTO { _id?: number; name: string; age: number; isEndangered?: boolean; }
While defining the DTO, I decided to make isEndangered
an optional field, so we will update our entity to set a default value for that attribute when none
is set:
// tree.entity.ts ... @Column({ default: false }) isEndangered: boolean; }
Now we can use this DTO class in our route definition:
// tree.controller.ts import { Controller, Get, Param, Post, Body } from '@nestjs/common'; import { Tree } from './tree.entity'; import { TreeService } from './tree.service'; import { CreateTreeDTO } from './dto/create-tree.dto'; @Controller('trees') export class TreeController { constructor(private readonly treeService: TreeService) {} ... @Post() createTree(@Body() body: CreateTreeDTO): Promise<Tree> { return this.treeService.create(body); } }
Next, let’s look at sending queries into routes. We’ll extend our list of all endpoints to accept queries with which we can filter our listing results.
To start, we’ll import Query
from @nestjs/common
. Then we will pass it to our controller method, like so:
// tree.controller.ts import { Controller, Get, Param, Post, Body, Query } from '@nestjs/common'; import { Tree } from './tree.entity'; import { TreeService } from './tree.service'; import { CreateTreeDTO } from './dto/create-tree.dto'; @Controller('trees') export class TreeController { constructor(private readonly treeService: TreeService) {} @Get() getTrees(@Query() query: { isEndangered?: boolean }): Promise<Tree[]> { return this.treeService.findAll({ isEndangered: query.isEndangered }); } ...
Now that we’ve passed in the query option isEndangered
— which, as we defined, is passed to our controller method and service method — we will extend both TreeService
and TreeEntity
to accept and filter results by the query option:
// tree.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateTreeDTO } from './dto/create-tree.dto'; import { TreeFilterOptions } from './interfaces/filters'; import { Tree } from './tree.entity'; @Injectable() export class TreeService { constructor( @InjectRepository(Tree) private treeRepository: Repository<Tree>, ) {} findAll(filters: TreeFilterOptions): Promise<Tree[]> { return this.treeRepository.find({ where: { isEndangered: filters.isEndangered, }, }); }
You’ll notice we defined an interface for our filter options TreeFilterOptions
that matches our route query object, and is what will be sent from the controller. Here is the interface:
// interfaces/filters.ts export interface TreeFilterOptions { isEndangered?: boolean; }
An important part of defining routes in NestJS is making sure the data sent via parameters, queries, or payload bodies match what our application expects.
To start, we will import the packages class-validator
and class-transformer
by running the following command:
$ yarn add class-validator class-transformer
Now let’s extend our CreateTreeDTO
class for our POST /trees
request to use the validator decorators available in class-validator
:
// dto/tree.dto.ts import { IsNotEmpty, IsNumber } from 'class-validator'; export class CreateTreeDTO { _id?: number; @IsNotEmpty() name: string; @IsNumber() age: number; isEndangered?: boolean; }
In our example above, the IsNotEmpty
decorator makes sure the request does not accept an empty string as a valid input even though it is technically a string of length 0
. If a request is made with an empty string value, the response will match:
{ "statusCode": 400, "error": "Bad Request", "message": ["name must not be empty"] }
Note that NestJS also allows us to validate route parameters using pipes.
In this article, we looked at how routes and controllers are defined in NestJS. We reviewed examples of HTTP requests and explored how options like payload bodies, queries, and parameters can be passed in route requests using a sample API application.
You can find the entire code used in this article in my Github repo.
I hope you found this article useful. Please share any thoughts or questions you might have in the comment section!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.