Setting up
Installing dependencies
npm i @nestjs/typeorm typeorm sqlite3
Entity
Entity is a class that maps to a database table (or collection when using MongoDB). You can create an entity by defining a new class and mark it with @Entity()
user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() email: string; @Column() password: string; }
Each entity MUST have a primary column (or ObjectId column if are using MongoDB).
Repository
TypeORM supports the repository design pattern, so each entity has its own repository. These repositories can be obtained from the database connection.
Repository
is just like EntityManager
but its operations are limited to a concrete entity.
For example, user entity lists the different properties that a User has (no functionality).
On the other hand Users Repository defines methods to find, update, delete and create a User.
Creating an Entity
There are three steps to create an Entity
.
- Create an entity file, and create a class in it that lists all the properties that your entity will have
- Connect the entity to its parent module. This creates a repository.
- Connect the entity to the root connection (in app module)
user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() email: string; @Column() password: string; }
Auto Validation
- Bind
ValidationPipe
at the application lever to ensure all endpoitns are protected from receiving incorrect data.
src/main.ts
import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap();
- Create Dto to add validation rules by using decorators provided by the
class-validator
package.
src/users/dtos/create-user.dto
import { IsEmail, IsString } from 'class-validator'; export class CreateUserDto { @IsEmail() email: string; @IsString() password: string; }
- Create an endpoint by using
CreateUserDto
we created.
import { Body, Controller, Post } from '@nestjs/common'; import { CreateUserDto } from './dtos/create-user.dto'; @Controller('auth') export class UsersController { @Post('/signup') createUser(@Body() body: **CreateUserDto**) { console.log(body); } }
- In this fashion, any route that uses the
CreateUserDto
will automatically enforce theses validation rules.
Example 1
Request
request.http
### Create a new user POST http://localhost:3000/auth/signup content-type: application/json { "email": "[email protected]", "password": "secret" }
Response
HTTP/1.1 201 Created X-Powered-By: Express Date: Sat, 11 Jun 2022 05:15:06 GMT Connection: close Content-Length: 0
Example 2
Request
### Create a new user POST http://localhost:3000/auth/signup content-type: application/json { "email": "not an email", "password": "secret" }
Response
HTTP/1.1 400 Bad Request X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 77 ETag: W/"4d-jYTTf+uHDpur+UVxEWx/0ijBEQ0" Date: Sat, 11 Jun 2022 05:17:01 GMT Connection: close { "statusCode": 400, "message": [ "email must be an email" ], "error": "Bad Request" }
Creating and Saving a User
- import UsersRepository into UsersService
- define a constructor method and list all dependencies
- define create method
import { Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './user.entity'; @Injectable() export class UsersService { constructor(@InjectRepository(User) private repo: Repository<User>) {} create(email: string, password: string) { const user = this.repo.create({ email, password }); return this.repo.save(user); } }
Why do we bother first creating user entity and then save it?
Case 1: First creating an instnace of user entity and then saving it
src/users/users.service.ts
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class UsersService { constructor(@InjectRepository(User) private repo: Repository<User>) {} create(email: string, password: string) { const user = this.repo.create({ email, password }); return this.repo.save(user); } }
Case 2: Saving an user object directly by calling save()
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class UsersService { constructor(@InjectRepository(User) private repo: Repository<User>) {} create(email: string, password: string) { // const user = this.repo.create({ email, password }); return this.repo.save({ email, password ); } }
We use case 1
when we save an entity instnace in the DB because of the following reasons:
Reason 1: Additional Validation logics
src/users/users.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() **@IsEmail()** email: string; @Column() password: string; }
Usually, we do not put business logic inside of the enity file. However, there are some scenarios in which we might want to put in some validaiton logic inside of the entity.
For example, we might put a validator on the email property. We might tie our validation logic directly to an entity as opposed to the incoming DTO. If we want to run the validation, we must create an instance of user entity and then save it.
Reason 2 : Entity Listener
Any of your entities can have methods with custom logic that listen to specific entity events. You must mark those methods with special decorators depending on what event you want to listen to. (https://typeorm.io/listeners-and-subscribers#afterinsert)
Entity Listener Examples
-
@AfterLoad TypeORM will call it each time the entity is loaded using QueryBuilder or repository/manager find methods.
-
@BeforeInsert
TypeORM will call it before the entity is inserted using repository/manager
save
-
@AfterInsert
TypeORM will call it after the entity is inserted using repository/manager
save
-
@BeforeUpdate
TypeORM will call it before an existing entity is updated using repository/manager
save
. Keep in mind, however, that this will occur only when information is changed in the model. If you run save without modifying anything from the model,@BeforeUpdate
and@AfterUpdate
will not run -
@AfterUpdate
TypeORM will call it after an existing entity is updated using repository/manager
save
-
@BeforeRemove
TypeORM will call it before a entity is removed using repository/manager
remove
-
@AfterRemove
TypeORM will call it after the entity is removed using repository/manager
remove
-
etc
Let’s say whenever there is an inser, update or remove user entity event, we want to log the event. The following code will satisfy our purpose.
src/users/user.entity.ts
import { AfterInsert, AfterUpdate, AfterRemove, Entity, Column, PrimaryGeneratedColumn, } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() email: string; @Column() password: string; @AfterInsert() logInsert() { console.log(`Inserted User with id ${this.id}`); } @AfterUpdate() logUpdate() { console.log(`Updated User with id ${this.id}`); } @AfterRemove() logRemove() { console.log(`Removed User with id ${this.id}`); } }
If we first create the user and then save it, we can see that the logInsert() function with @AfterInsert()
hook was triggered.
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class UsersService { constructor(@InjectRepository(User) private repo: Repository<User>) {} create(email: string, password: string) { const user = this.repo.create({ email, password }); return this.repo.save(user); } }
As the result of a POST request that inserts a new user, we can see that the log was shown in the console.
If we save the entity instance, all the hooks tied to that instance will be executed. But if we pass in object and try to save it, no hooks will get executed.
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class UsersService { constructor(@InjectRepository(User) private repo: Repository<User>) {} create(email: string, password: string) { // const user = this.repo.create({ email, password }); // return this.repo.save(user); return this.repo.save({ email, password }); } }
To sum up,
save()
and remove()
are expected to be called withh entity instances, and if you call them with an entity, Hooke's will be executed.
But if you make use of insert()
, update()
or delete()
to directly insert a record, directly update or directly delete a record, then your hooks will not be executed, which is sometimes not going to be what you expect.
Update
When updating an entity, the updated fields of entity mgiht vary. For example, if we are updating an user that has email and password fields, we can expect three scenarios: updating only an email, updating only a password, updating both email and password.
In this case, we can create dto that uses @IsOptional()
of class-validator
In User’s case I mentioned above, we can create update-user.dto.ts
as follows.
src/users/dtos/update-user.dto.ts
import { IsEmail, IsString, IsOptional } from 'class-validator'; export class UpdateUserDto { @IsEmail() @IsOptional() email: string; @IsString() @IsOptional() password: string; }
src/users/users.controller.ts
import { ... Patch, ... } from '@nestjs/common'; import { UsersService } from './users.service'; import { UpdateUserDto } from './dtos/update-user.dto'; @Controller('auth') export class UsersController { constructor(private usersService: UsersService) {} ... @Patch('/:id') updateUser(@Param('id') id: string, @Body() body: UpdateUserDto) { return this.usersService.update(+id, body); }
src/users/users.service.ts
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { throwIfEmpty } from 'rxjs'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class UsersService { constructor(@InjectRepository(User) private repo: Repository<User>) {} ... findOne(id: number) { return this.repo.findOne({ where: { id } }); } ... async update(id: number, attrs: Partial<User>) { const user = await this.findOne(id); if (!user) { throw new Error('user not found.'); } Object.assign(user, attrs); return this.repo.save(user); } ... }
save () vs update() / insert()
hooks are executed / hooks are not executed (uses plain object)
Exceptions
Exception filters Nest comes with a built-in exceptions layer which is responsible for processing all unhandled exceptions across an application. When an exception is not handled by your application code, it is caught by this layer, which then automatically sends an appropriate user-friendly response.
src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './user.entity'; @Injectable() export class UsersService { constructor(@InjectRepository(User) private repo: Repository<User>) {} ... async updaet(id: number, attrs: Partial<User>) { const user = await this.findOne(id); if (!user) { // throw new Error('user not found.'); throw new NotFoundException('user not found'); } Object.assign(user, attrs); return this.repo.save(user); } async remove(id: number) { const user = await this.findOne(id); if (!user) { // throw new Error('user not found.'); throw new NotFoundException('user not found'); } return this.repo.remove(user); } }
Result
request.http
### delete a auser DELETE http://localhost:3000/auth/456
response
HTTP/1.1 404 Not Found X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 65 ETag: W/"41-UFEDmPZKghYvLGIvFt5ynukJz7w" Date: Wed, 15 Jun 2022 13:25:17 GMT Connection: close { "statusCode": 404, "message": "user not found", "error": "Not Found" }
request.http
### delete a auser DELETE http://localhost:3000/auth/3
response
HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 45 ETag: W/"2d-UNCsQBZi2HjwkdZN2e8dDbV4Jy0" Date: Wed, 15 Jun 2022 13:26:12 GMT Connection: close { "email": "[email protected]", "password": "secret" }
Custom Data Serialization
For now, findUser()
controller returns user’s information including the password, which might not be the intended behavior. Sometimes, we need to filter out sensitive or unneccessary data from responses.
users/users.controller.ts
@Get('/:id') findUser(@Param('id') id: string) { return this.usersService.findOne(+id); }
Request
### Find a user by id GET http://localhost:3000/auth/2
Response
HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 55 ETag: W/"37-fbkBjDrwzRBuQcsVceO3Jra2MVk" Date: Fri, 29 Jul 2022 13:42:51 GMT Connection: close { "id": 2, "email": "[email protected]", "password": "tester123" }
NestJS provides a way to exclude response properties.
Documentation | NestJS - A progressive Node.js framework
As mentioned inthe NestJS documentation, we can use ClassSerializerInterceptor
to apply rules expressed by class-transformer
decorators on an entity/DTO class.
First, in the entity file, we annotate the entity we want to exclude from the response as follows:
users/users.entity.ts
import { Exclude } from 'class-transformer'; import { Column, Entity, PrimaryGeneratedColumn, } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() email: string; @Column() @Exclude() password: string; ... }
Note that we included Exclude
from class-transforer
and used @Exclude()
decorator to the target property.
And we can use @UserInterface(ClassErializerInterceptor)
to the contorller that returns the instance of this entity. This will make the app apply rules expressed by class-transformer decorators on an entity class.
users/users.controller.ts
@UseInterceptors(ClassSerializerInterceptor) @Get('/:id') findUser(@Param('id') id: string) { return this.usersService.findOne(+id); }
Request
### Find a user by id GET http://localhost:3000/auth/2
Resposne
HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 32 ETag: W/"20-OieX8llTxVyn25/DqLzbCp9cTZU" Date: Fri, 29 Jul 2022 14:02:20 GMT Connection: close { "id": 2, "email": "[email protected]" }
The response excluded password
property as descibed in the entity file.
An Issue with using ClassSerializerInterceptor
There is an issue with using ClassSerializerInterceptor as mentioned in the NestJS document. Think about the following situation. User’s service returns informations tied to a user. Through admin route, we might want to get extra properties tied to a user. And the public route should expose only non-sensitive information. If we use the way introduced in the NestJS document to serialize response, there is no way to serve different data for admin routes and public routes through one method that returns user information.
This issue is handled in the ‘Serialization’ post. You can check my post here