NestJS is a backend framework for creating scalable applications. It works as an abstraction over the underlying HTTP server library. Currently, it supports two libraries — Express and Fastify — while still enabling developers to use their own APIs when needed.
More importantly, it forces developers to use a specific architecture by introducing Angular-like modules, services, and controllers, ensuring the application is scalable, highly testable, and loosely coupled.
Express, Koa, and hapi don’t enforce any particular architecture. They provide a set of lower-level tools, sure, but still leave a lot up to the developers. A mistake early on in the project regarding the architecture may cost a lot in terms of time spent refactoring the codebase later.
Conversely, Nest is strict when it comes to the codebase — its modules, services, and controllers — and you can’t really go wrong with it.
Working on a project as a team means there are a lot of preferences floating around regarding how an application should be structured. All these preferences forced into one codebase won’t provide any enhancements. Instead, it will cause a mess and overall poor code quality.
To combat this, Nest provides a standardized set of guidelines by defining an opinionated architecture that each developer in the team has to follow. The result is an easily maintainable codebase.
Nest abstracts away all the dealings with underlying libraries such as Express or Fastify by introducing a few simple building blocks, the most important among them being modules, services, and controllers.
Modules encapsulate all the logic pertaining to a given domain. Say we needed to implement user-related logic. We could create a UserModule
that would contain UserService
, UserController
.
The module itself is just a class decorated with the @Module
decorator, in which we provide all the necessary metadata. If we wanted to access a given part of the module from other modules, we could export that part in the exports
array.
Here’s the CatModule
from the official documentation:
import { Module } from '@nestjs/common'; import { CatsController } from './cats.controller'; import { CatsService } from './cats.service'; @Module({ controllers: [CatsController], providers: [CatsService], exports: [CatsService] }) export class CatsModule {}
A typical application will have modules such as ApplicationModule
(the root module), SharedModule
(which would further encapsulate all the underlying reusable, across-the-application modules), and, like the one provided above, modules that will encapsulate the logic of the public API.
Read more about NestJS modules here.
A controller is a class annotated with the @Controller
decorator. Controllers act as a layer between incoming HTTP request and the corresponding logic handling it. We can think of a controller as a middleman: it checks what request comes in and calls the appropriate service’s method.
Controllers define the routing paths, arguments, responses, and everything else there is related to an HTTP request. They don’t care about what’s going on between the request and the response.
@Controller('cats') export class CatsController { @Post() create(@Body() createCatDto: CreateCatDto) { return 'This action adds a new cat'; } @Get() findAll(@Query() query: ListAllEntities) { return `This action returns all cats (limit: ${query.limit} items)`; } @Get(':id') findOne(@Param('id') id: string) { return `This action returns a #${id} cat`; } @Put(':id') update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) { return `This action updates a #${id} cat`; } @Delete(':id') remove(@Param('id') id: string) { return `This action removes a #${id} cat`; } }
In the example above, there is a controller listening on the application_url/cats
path. Each method is decorated with the @Get
, @Post
, @Put
, or @Delete
to indicate what HTTP method they are interested in. We can also provide an additional path inside the decorator.
Instead of using the request object directly to access the body or a query (as we would in Express), each argument can be decorated with @Param
, @Query
, or @Body
, and thus the underlying access layer can be abstracted.
Although each method in the example above works synchronously, methods may return asynchronous values with promises and observables.
Read more about controllers here.
A service is a class annotated with the @Injectable
decorator. It contains domain (business) logic. By separating the access layer (controllers) and logic layer (services), we have a clear separation of concerns.
@Injectable() export class CatsService { private readonly cats: Cat[] = []; create(cat: Cat) { this.cats.push(cat); } findAll(): Cat[] { return this.cats; } }
By using the @Injectable
decorator, we are telling Nest that this class may be injected into other parts of the application.
Read more about services.
Dependency injection is one of the most important aspects of Nest. By providing the support out of the box, Nest allows us to write loosely coupled code, which, in turn, is also easily testable.
Thanks to Nest being written in TypeScript, the dependencies are resolved just by type — there’s no need to inject them by hand!
@Controller('cats') export class CatsController { constructor(private readonly catsService: CatsService) {} @Post() async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } @Get() async findAll(): Promise<Cat[]> { return this.catsService.findAll(); } }
Instead of providing each of our classes that depends on the CatsService
with new CatsService(...deps)
, we are telling Nest, “Should someone ask for an instance of this class in their dependencies, create it for them.”
By default, the provided instances are singletons and are shared between each requesting entity, but that can be changed by specifying the injection scope. Our dependencies are usually defined in the constructor by using the private readonly name: type
syntax.
Read more about dependency injection in Angular’s dependency injection guide.
It is highly recommended that the structure of the codebase be reflected in the folder structure. A module should have its own separate folder in which all of its internal building blocks reside.
Nest, following in the steps of Angular, also has a naming convention to include the appropriate file ending such as .controller
, .service
, and .module
. This way, the project’s folder structure is easily readable and gives the developer a good overview of the codebase.
Nest comes with its own CLI.
npm i -g @nestjs/cli
The most important commands are new
and generate
.
The new
command lets us create a whole starting application’s boilerplate with Nest in a matter of seconds.
nest new application-name
The generate
command generates a set of files for a requested feature. It might also modify the existing ones if necessary.
nest generate service cat
In the example above, there would be two files generated: cat.service.ts
and cat.service.spec.ts
. Also, if there was a cat
module specified in the directory, the service would be automatically imported.
The CLI really increases developers’ productivity by reducing the amount of boilerplate that needs to be written for each building block and by generating a .spec
file that has some very basic tests written in it.
Tests are very important to ensure that an application keeps working correctly once some changes are made. Nest takes testing very seriously and provides testing utilities in order to make the process as smooth as possible. By using dependency injection, it is very easy to mock out the modules we are not currently testing thanks to Nest’s custom providers feature.
Here’s an example of unit testing the CatsController
:
import { Test } from '@nestjs/testing'; import { CatsController } from './cats.controller'; import { CatsService } from './cats.service'; describe('CatsController', () => { let catsController: CatsController; let catsService: CatsService; beforeEach(async () => { const module = await Test.createTestingModule({ controllers: [CatsController], providers: [CatsService], }).compile(); catsService = module.get<CatsService>(CatsService); catsController = module.get<CatsController>(CatsController); }); describe('findAll', () => { it('should return an array of cats', async () => { const result = ['test']; jest.spyOn(catsService, 'findAll').mockImplementation(() => result); expect(await catsController.findAll()).toBe(result); }); }); });
As we mentioned before, Nest’s CLI generates basic tests for each service, controller, filter, etc. By doing so, Nest gives us not only the necessary tools, but also a reminder that we should test each of our application’s features.
If you would like to see some more complicated cases (i.e., mocking a database connection), I have created an application you can see here.
Nest stays on top of the new trends and makes it very easy to write an application based on the microservices architecture.
Although by default Nest is used for building REST APIs, thanks to its platform-agnostic approach, the architecture can be used to create a GraphQL API as well. When it comes to documenting our REST API, there’s a great integration with the Swagger library that lets us generate the documentation effortlessly.
There are also many packages in the Nest ecosystem that integrate the existing packages into the Nest architecture. The most popular packages include typeorm, passport, and mongoose. If a given library is not supported, we could always create our own service/provider that would encapsulate said library in a Nest-accessible way.
Nest is not a be-all and end-all, of course, and does fall short in some cases, or the steps necessary to achieve expected results are unclear. But with its ever-growing community (more than 20,000 stars on GitHub), it becomes easier and easier to get a response to the burning questions quickly.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
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 nowDesign React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
From basic syntax and advanced techniques to practical applications and error handling, here’s how to use node-cron.
2 Replies to "Take your Node backend to the next level with NestJS"
very good
thanks @ team