Building Reusable CRUD APIs with TypeORM and NestJS

If you're building a Node.js application with NestJS and TypeORM, you'll likely need to create CRUD (Create, Read, Update, Delete) APIs for multiple entities. However, creating these APIs from scratch for each entity can be time-consuming and lead to a lot of code duplication. That's where the repository pattern comes in - it can help you abstract away the database access layer and simplify your business logic layer, while also allowing you to create reusable CRUD APIs.

In this blog post, we'll walk through how to implement a repository pattern in your NestJS project using TypeORM. We'll create a base repository class that implements common CRUD methods using the TypeORM repository, and a generic CRUD service class that uses a generic repository interface to provide common CRUD methods. We'll then show how to use this solution in a NestJS controller to create a complete CRUD API.

Creating a Base Entity and Repository

To start, we'll create a base entity class that all other entities will extend. This class will define common properties that all entities will have. We'll also use TypeORM decorators to define the database schema for this entity:

import { BaseEntity as TypeOrmBaseEntity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

export abstract class BaseEntity extends TypeOrmBaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

Next, we'll create a base repository class that all other repositories will extend. This class will define common CRUD methods that all entities will have:

import { Repository } from 'typeorm';
import { BaseEntity } from './base.entity';

export abstract class BaseRepository<T extends BaseEntity> {
  constructor(private readonly repository: Repository<T>) {}

  async findAll(): Promise<T[]> {
    return this.repository.find();
  }

  async findById(id: number): Promise<T> {
    return this.repository.findOne(id);
  }

  async create(data: Partial<T>): Promise<T> {
    const entity = this.repository.create(data);
    return this.repository.save(entity);
  }

  async update(id: number, data: Partial<T>): Promise<T> {
    await this.repository.update(id, data);
    return this.findById(id);
  }

  async delete(id: number): Promise<void> {
    await this.repository.delete(id);
  }
}

This class takes a Repository instance as a constructor parameter, which it uses to implement common CRUD methods. Note that the T type parameter is constrained to entities that extend the BaseEntity class.

Creating a Generic CRUD Service

Now that we have a base repository class, we'll create a generic CRUD service class that all other services will extend. This class will use a generic repository interface to provide common CRUD methods that all entities will have:

import { IRepository } from './irepository';
import { BaseEntity } from './base.entity';

export abstract class BaseService<T extends BaseEntity> {
  constructor(private readonly repository: IRepository<T>) {}

  async findAll(): Promise<T[]> {
    return this.repository.findAll();
  }

  async findById(id: string): Promise<T> {
    return this.repository.findById(id);
  }

  async create(data: Partial<T>): Promise<T> {
    return this.repository.create(data);
  }

  async update(id: string, data: Partial<T>): Promise<T> {
    return this.repository.update(id, data);
  }

  async delete(id: string): Promise<void> {
    return this.repository.delete(id);
  }
}

This class a IRepository<T> instance as a constructor parameter, which defines the common CRUD methods that all entities will have. The T type parameter is constrained to entities that extend the BaseEntity class.

We'll also create an interface for the generic repository that the base repository class will implement:

import { BaseEntity } from './base.entity';

export interface IRepository<T extends BaseEntity> {
  findAll(): Promise<T[]>;
  findById(id: string): Promise<T>;
  create(data: Partial<T>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

This interface defines the same common CRUD methods as the BaseRepository class.

Using the Generic CRUD Service in a Controller

Now that we have a generic CRUD service, we'll use it in a NestJS controller to create a complete CRUD API for an entity. First, we'll create an entity class that extends the BaseEntity class:

import { BaseEntity } from './base.entity';
import { Entity, Column } from 'typeorm';

@Entity()
export class User extends BaseEntity {
  @Column()
  name: string;

  @Column()
  email: string;

  @Column()
  password: string;
}

Next, we'll create a repository class that extends the BaseRepository class and implements the IRepository<User> interface:

import { Repository } from 'typeorm';
import { BaseRepository } from './base.repository';
import { User } from './user.entity';
import { IRepository } from './irepository';

export class UserRepository extends BaseRepository<User> implements IRepository<User> {
  constructor(repository: Repository<User>) {
    super(repository);
  }
}

This class takes a Repository<User> instance as a constructor parameter, which it passes to the BaseRepository class.

Finally, we'll create a controller that uses the BaseService class to implement common CRUD endpoints for the User entity:

import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { User } from './user.entity';

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

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

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

  @Post()
  async create(@Body() data: Partial<User>): Promise<User> {
    return this.userService.create(data);
  }

  @Put(':id')
  async update(@Param('id') id: string, @Body() data: Partial<User>): Promise<User> {
    return this.userService.update(id, data);
  }

  @Delete(':id')
  async delete(@Param('id') id: string): Promise<void> {
    return this.userService.delete(id);
  }
}

This controller takes a UserService instance as a constructor parameter, which it uses to implement common CRUD endpoints for the User entity. Note that the UserService class extends the BaseService<User> class and takes a UserRepository instance as a constructor parameter:

import { Injectable } from '@nestjs/common';
import { BaseService } from './base.service';
import { User } from './user.entity';
import { UserRepository } from './user.repository';

@Injectable()
export class UserService extends BaseService<User> {
  constructor(repository: UserRepository) {
    super(repository);
  }
}

This class takes a UserRepository instance as a constructor parameter, which it passes to the `BaseService class

Conclusion

In this blog post, we've shown how to create a reusable CRUD API function in TypeORM and NestJS using the repository pattern. We've created a base repository class and a generic CRUD service class that can be used to implement common CRUD endpoints for any entity. We've also shown how to use these classes in a NestJS controller to create a complete CRUD API for an entity.

By using the repository pattern and creating reusable code, we can save a lot of time and effort when implementing APIs for multiple entities. We can also ensure consistency and reduce the risk of errors by using a common interface and implementation for CRUD operations.

If you're interested in learning more about TypeORM and NestJS, be sure to check out their documentation and tutorials. They're both powerful tools that can help you build scalable and maintainable APIs.

Source Code

reuseable-crud