Ryan Kim

Menu

How to use TypeORM with NestJS

How to use TypeORM with NestJS

Oct 15, 2022

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.

  1. Create an entity file, and create a class in it that lists all the properties that your entity will have
  2. Connect the entity to its parent module. This creates a repository.
  3. 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

  1. 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();
  1. 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;
}
  1. 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

  1. import UsersRepository into UsersService
  2. define a constructor method and list all dependencies
  3. 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?

typeorm-create-and-save.png

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);
  }
}

save-entity-instnace-request.png

As the result of a POST request that inserts a new user, we can see that the log was shown in the console.

save-entity-instnace-response.png

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 });
  }
}

save-object-response.png

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.

issue-with-using-class-serializer-interceptor.png

This issue is handled in the ‘Serialization’ post. You can check my post here