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.