I mostly used Firebase for my backend for my projects because it provided a lot of tools that made it easy to create a backend. Recently, though, I've been gaining some interest in backend development and decided to look into servers and databases. The backend framework I decided to use is Nestjs. To give a brief introduction to Nestjs, it's a framework that was inspired by Angular so it is very similar syntactically which is a big reason why I chose it and it also uses Expressjs under the hood. It also provides a lot of structure for my projects in comparison to Express which made it easy for a beginner like myself.
From what I have gathered online, backend development, on the very basic level, is composed of the server and the database. This post will go over what a server does and how Nestjs works as a server.
What does a Server do?
When the browser sends an HTTP request, the server sends back an appropriate response. But before responding it does the following:
- Validation: validates the data from the response
- Authentication: verifies the user's credentials
- Routing: routes the request to the relevant function
- Business logic: the function mentioned in routing
- Access a database
Nestjs provides the following features for each function mentioned above:
- Pipe - validation
- Guard - authentication
- Controller - routing
- Service - business logic
- Repository - accessing a database
Controllers
How does a controller work?
When a user interacts with the frontend it makes requests to the backend to do something for them. The main request types are GET, POST, PUT, and DELETE which follows the REST architectural style. These methods basically tell the backend to either create, read (get), update, or delete data.
How does Nestjs's controller work?
Nestjs provides GET, PUT, POST, DELETE, and etc. decorators to route the requests. The following is an example of how a controller would look like in Nestjs.
import { Controller, Get, Post } from "@nestjs/common"
@Controller('/messages')
export class UserController {
// GET
// /messages
@Get() // the "Get" decorator
getMessages() // the function that gets executed
// GET
// /messages/:id
@Get('/:id')
getSingleMessage()
// POST
// /messages
@Post()
updateMessage()
}
When the request matches the request type (Get, Post, etc.) and the route (e.g. "/messages") then Nestjs executes the function below the decorator.
How do you extract information from the request?
Let's begin with an example of why we would need to extract information. When a browser POSTs some info, the server needs to get a hold of the body of the data in order to store it or to run some business logic on it. We can get a hold of not only the data but also the parameter and queries.
What does a request (HTTP request) look like?
A request consists of the following:
- Start line: request type and route
- Header: Content-type, host, etc.
- Body: data
Start line: POST /users/5?validate=true // Start line
HOST: localhost:4200 // Header
Content-Type: application/json // Header
Body: {"content": "hi!"} // Body
How does Nestjs allow you to access the data in a HTTP request?
Nestjs provides decorators to access the data you need.
@Param('id') // this would get you the "5" in the route below
@Query() // this would get you the "validate=true" in the route below
Start line: POST /users/5?validate=true
@Headers()
HOST: localhost:4200
Content-Type: application/json
@Body()
Body: {"content": "hi!"}
Let's use the decorators in the example we used above.
import { Controller, Get, Post, Body } from "@nestjs/common"
@Controller('/messages')
export class MessagesController {
// GET
// /messages
@Get()
getMessages()
// GET
// /messages/:id
@Get('/:id')
getSingleMessage(@Param('id') id: string) {
console.log(id) // this would print the id in the request
}
// POST
// /messages
// {"content": "im a user"}
@Post() // use the body decorator here to access the data in the HTTP request
updateUser(@Body() body: any) {
console.log(body) // this would print the body in the request
}
}
Pipes
How does a pipe work?
The pipe runs before the request gets to the controller to validate the data. For example, if a POST request has a body that contains a number but the controller can only accept a string. Then the pipe would reject it before it gets to the controller.
Nestjs provides a built-in ValidationPipe that has many commonly used validations. To use the pipe, you just have to create a class the describes the different properties that a request body should have.
Pipe classes are usually called a Data Transfer Objects("Dto").
Let's see how you would use this in the example above.
import { IsString } from 'class-validator'
export class MessageDto {
@IsString()
content: string;
}
import { Controller, Get, Post, Body } from "@nestjs/common"
import { MessageDto } from './messageDto'
@Controller('/messages')
export class MessagesController {
// GET
// /messages
@Get()
getMessages()
// GET
// /messages/:id
@Get('/:id')
getSingleMessage(@Param('id') id: string) {
console.log(id)
}
// POST
// /messages
// {"content": "im a user"}
@Post()
updateUser(@Body() body: MessageDto) {
// we replaced the type with the MessageDto. That's all we need to do to use the pipe
console.log(body)
}
}
Repository
The repository is a class used to interact with the database. This part goes into TypeORM which is explained in this post.
Service
Services are where all of the business logic is located. It also uses the methods in the repository class to interact with the database. Many methods in the service are similar to the methods in the repository and may appear redundant but this structure of separating the business logic from the interaction with the database offers benefits of writing easier test code, finding bugs, and etc.
export class MessageService {
async findOne(id: string) {
return this.messageRepository.findOne(id);
}
async findAll() {
return this.messageRepository.findAll();
}
async create(content: string) {
return this.messageRepository.create(content);
}
}
Now we have to go over how Nestjs combines all these features together to maintain a project. One of the most important concepts is dependency injection. In simple terms, each class depends on some other class and needs to get that class that it depends on injected into it. I go into further detail below.
Inversion of Control ("IoC")
I'm going over IoC before dependency injection because this is the principle that the dependency injection design pattern is trying to implement.
The IoC basically states that classes should not create instances of its dependencies on its own. Instead, they should get their dependencies from an outside source. This would help classes become more reusable as projects scale.
In Nest, there is a Nest IoC Container that does this job for us which is further explained below.
export class MessageController {
constructor(private messageService = new MessageService()) {} // this would be a violation of the IoC principle because an instance is created manually
constructor(private messageService: MessageService) {} //this method is encouraged. You would have to import it and include it in the appropriate module provider to allow the Nest IoC Container know that it's a dependency injection.
}
Dependency Injection ("DI")
Nestjs revolves around DI. So I think it'd be helpful to understand how Nestjs uses it.
First, let's go over how our different classes depend on each other in our example.
MessageController --> MessageService --> MessageRepository
As seen in the diagram above, the messageController class depends on the messageService class and the messageService class depends on the messageRepository class to work properly.
The Nest IoC Container records all these classes and their dependencies. After it is done recording, it creates instances of all the required dependencies and ultimately returns the controller instance. The important benefit of using this container is that Nest reuses the instances that it created. So, if a project grows and there are multiple controllers that need a certain service, Nest would use the already created instance of the service instead of creating another one.
Top comments (0)