Use TypeOrm Custom Repository in NestJS

Reading Time: 5 minutes

Loading

NestJS has out of box integration with TypeORM, a TypeScript and JavaScript ORM that has good support of PostgreSQL. The library offers decorators to model data types and entities and tools to generate migration scripts to update database schema; therefore, it is a popular choice to perform CRUD operations.

TypeORM offers custom repository that allows developer to extend a repository and add custom methods. It is a powerful feature because developers can inject custom repository in a service and call custom methods as if they are built-in. Moreover, it is easier to mock custom methods than builder methods such as leftJoinAndSelect() and getMany() in unit test.

Our company uses nestJS at work and we recently adopted custom repository to encapsulate TypeORM queries in our custom repositories. I am going to illustrate how the team did it for the rest of the post.

Step 1: Create docker-compose.yml to pull PostgreSQL image to docker

docker-compose.yml

version: '3.7'

services:
  postgres:
    container_name: postgres_container2
    image: postgres:12.7-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      PGDATA: /data/postgres
    volumes:
      - postgres:/data/postgres
    ports:
      - '5433:5432'
    restart: always
    networks:
      - nest-poc

  pgadmin:
    container_name: pgadmin_container2
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org
      PGADMIN_DEFAULT_PASSWORD: pgadmin
    volumes:
      - pgadmin:/root/.pgadmin
    ports:
      - '5556:80'
    links:
      - 'postgres:pgsql-server'
    restart: always
    networks:
      - nest-poc

volumes:
  postgres:
  pgadmin:

networks:
  nest-poc:
    name: nest-poc

Step 2: Install TypeORM and PostgreSQL dependencies

npm install --save @nestjs/typeorm typeorm pg

ormconfig.json

{
    "type": "postgres",
    "host": "localhost",
    "port": 5433,
    "username": "postgres",
    "password": "postgres",
    "entities": ["dist/**/*.entity{.ts,.js}"],
    "migrations": ["dist/migrations/**/*.js"],
    "synchronize": false,
    "cli": {
      "migrationsDir": "src/migrations"
    }
}

Step 3: Create entities, custom repositories and repository module

Depends on your preference, our company creates a repository module that defines all the entities and custom repositories used in the project. Then, we export them from the module such that they are accessible to other modules.

user.entity.ts

import {
  Column,
  CreateDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
  VersionColumn,
} from 'typeorm'
import { Report } from './report.entity'

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ type: 'text' })
  name: string

  @Column({ type: 'text' })
  lastname: string

  @Column({ type: 'integer' })
  age: number

  @CreateDateColumn({ type: 'timestamp with time zone' })
  createdAt: Date

  @UpdateDateColumn({ type: 'timestamp with time zone' })
  updatedAt: Date

  @VersionColumn({ type: 'integer', default: 1 })
  version: number

  @OneToMany(() => Report, (report) => report.owner)
  reports: Report[]
}

user.repository.ts

import { NewUser } from './type'
import { User } from '@/entities'
import { EntityRepository, Repository } from 'typeorm'

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async getUsers(): Promise<User[]> {
    return this.find({
      order: {
        name: 'ASC',
        lastname: 'ASC',
      },
    })
  }

  async getUser(id: string): Promise<User | undefined> {
    return this.findOne(id)
  }

  async createUser(newUser: NewUser): Promise<User> {
    return this.save(this.create(newUser))
  }

  async getUserWithReports(id: string): Promise<User | undefined> {
    return this.createQueryBuilder('user')
      .leftJoinAndSelect('user.reports', 'reports')
      .where('user.id = :id', { id })
      .getOne()
  }
}
repositories.module.ts

import { ReportRepository } from './report.repository'
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { User, Report } from '@/entities'
import { UserRepository } from './user.repository'

@Module({
  imports: [TypeOrmModule.forFeature([User, Report, UserRepository, ReportRepository])],
  exports: [TypeOrmModule],
})
export class RepositoryModule {}

Step 4: Create new modules to use repository module

We create user and report modules and import RepositoryModule in UserModule and ReportModule respectively.

user.module.ts

import { RepositoryModule } from '@/repositories'
import { Module } from '@nestjs/common'
import { UserService } from './services'
import { UserController } from './user.controller'

@Module({
  imports: [RepositoryModule],
  providers: [UserService],
  controllers: [UserController],
})
export class UserModule {}

We create user controller and user service to perform user actions. Similarly, there are report controller and report service to handle report actions

user.service.ts

import { User } from '@/entities'
import { UserRepository } from '@/repositories'
import { Injectable } from '@nestjs/common'
import { CreateUserDto } from '../dtos'

@Injectable()
export class UserService {
  constructor(private userRepository: UserRepository) {}

  async getUsers(): Promise<User[]> {
    return this.userRepository.getUsers()
  }

  async getUser(id: string): Promise<User | undefined> {
    return this.userRepository.getUser(id)
  }

  async getUserWithReports(id: string): Promise<User | undefined> {
    return this.userRepository.getUserWithReports(id)
  }

  async createUser(dto: CreateUserDto): Promise<User> {
    return this.userRepository.createUser(dto)
  }
}

user.controller.ts

import { UserService } from './services'
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { User } from '@/entities'
import { CreateUserDto } from './dtos'

@Controller('user')
export class UserController {
  constructor(private userService: UserService) {}

  @Get()
  async getUsers(): Promise<User[]> {
    return this.userService.getUsers()
  }

  @Get(':id/report')
  async getUserWithReports(@Param('id') id: string): Promise<User | undefined> {
    return this.userService.getUserWithReports(id)
  }

  @Get(':id')
  async getUser(@Param('id') id: string): Promise<User | undefined> {
    return this.userService.getUser(id)
  }

  @Post()
  async createUser(@Body() dto: CreateUserDto): Promise<User> {
    return this.userService.createUser(dto)
  }
}

We use Postman/CURL to call endpoints to ensure the logic works and data is persisted in database

curl "http://localhost:3000/user"
curl "http://localhost:3000/user/1/report"

Step 5: Unit test service and controller

Earlier in the post, I claimed that mocking custom repository is easier than mocking builder methods of TypeORM. Let’s write some test cases to test user service and user controller to show it.

user.service.spec.ts

describe('UserService', () => {
  let service: UserService
  let spyUserRepository: UserRepository

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: UserRepository,
          useValue: {},
        },
      ],
    }).compile()

    service = module.get<UserService>(UserService)
    spyUserRepository = module.get<UserRepository>(UserRepository)
  })

  it('should be defined', () => {
    expect(service).toBeDefined()
  })

  describe('getUsers', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return all users', async () => {
      const users: User[] = [
        { id: v4(), name: 'John', lastname: 'Doe', age: 10, createdAt: now, updatedAt: now, version: 1, reports: [] },
        { id: v4(), name: 'Jane', lastname: 'Doe', age: 22, createdAt: now, updatedAt: now, version: 1, reports: [] },
      ]
      spyUserRepository.getUsers = jest.fn(() => Promise.resolve(users))
      const result = await service.getUsers()
      expect(result).toEqual(users)
    })
  })

  describe('createUser', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return a new user', async () => {
      const id = v4()
      const newUserDto = {
        name: 'hello',
        lastname: 'world',
        age: 88,
      }

      const newUser: User = { ...newUserDto, id, createdAt: now, updatedAt: now, version: 1, reports: [] }

      spyUserRepository.createUser = jest.fn(() => Promise.resolve(newUser))
      const result = await service.createUser(newUserDto)
      expect(result).toEqual(newUser)
    })
  })
})
user.controller.spec.ts

describe('UserController', () => {
  let controller: UserController
  let userService: UserService

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UserController],
      providers: [
        UserService,
        {
          provide: UserRepository,
          useValue: {},
        },
      ],
    }).compile()

    controller = module.get<UserController>(UserController)
    userService = module.get<UserService>(UserService)
  })

  it('should be defined', () => {
    expect(controller).toBeDefined()
  })

  describe('getUsers', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return all users', async () => {
      const users: User[] = [
        { id: v4(), name: 'John', lastname: 'Doe', age: 10, createdAt: now, updatedAt: now, version: 1, reports: [] },
        { id: v4(), name: 'Jane', lastname: 'Doe', age: 22, createdAt: now, updatedAt: now, version: 1, reports: [] },
      ]

      jest.spyOn(userService, 'getUsers').mockImplementation(() => Promise.resolve(users))
      const result = await controller.getUsers()
      expect(result).toEqual(users)
    })
  })

  describe('createUser', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return a new user', async () => {
      const id = v4()
      const newUserDto = {
        name: 'hello',
        lastname: 'world',
        age: 88,
      }

      const newUser: User = { ...newUserDto, id, createdAt: now, updatedAt: now, version: 1, reports: [] }

      jest.spyOn(userService, 'createUser').mockImplementation(() => Promise.resolve(newUser))
      const result = await controller.createUser(newUserDto)
      expect(result).toEqual(newUser)
    })
  })
})

This is the end of the blog post! I hope you enjoy reading it and NestJS development.

Resources:

  1. Repo: https://github.com/railsstudent/nest-poc-standard-version