When I was working with the Django rest framework, there was a class called ModelViewSet, just inheriting this class and specifying your database model name, all the CRUD endpoints would be created for you automatically. I really liked this idea as it gives you consistency look to your API, less code has to be written and finally lets you focus more on the business logic.
This article will provide implementation to a generic class that when it is extended will provide similar functionality to ModelViewSet.
In order to do that TypeORM library is used, as our model library. As an example, I will do the classic Article model and will show how I will do the endpoints for it, as the model would be:
@Entity('article')
export class ArticleEntity extends BaseEntity {
@PrimaryGeneratedColumn({ name: 'article_id' })
articleId: number;
@Column()
title: string;
@Column()
body: string;
}
The controller of this model with a GET endpoint would be normally like this:
@Controller({ path: 'article' })
class ArticleController {
constructor(
@InjectRepository(ArticleEntity)
protected readonly repository: Repository<ArticleEntity>,
) {}
@Get(':id')
async get(@Param('id', ParseIntPipe) id: number):
Promise<ArticleEntity> {
return this.repository.findOne({ where: { articleId: id } });
}
}
Now we have to create an abstract generic class with a GET function, I will call it EndPointSet, and it would look like this without implementation:
abstract class EndPointSet<T extends BaseEntity> {
protected abstract readonly repository: Repository<T>;
@Get(':id')
async get(@Param('id', ParseIntPipe) id: number): Promise<T> {
// IMPLEMENTATION HERE, KEEP READING ^^
}
}
But now we have a problem, each different model would have a different id field name, so we can not use the findOne method, luckily Typeform provides the findOneById method, which needs only the id value, not its name, so the method implementation would be:
return this.repository.findOneById(id);
And now we can inherit the EndPointSet not only in the ArticleController but any controller as follows:
@Controller({ path: 'article' })
export class ArticleController extends EndPointSet<ArticleEntity> {
constructor(
@InjectRepository(ArticleEntity)
protected readonly repository: Repository<ArticleEntity>,
) {super();}
}
The other endpoints will be similar to what we did in the Get function, so the final class at the end would be like that:
abstract class EndPointSet<T extends BaseEntity> {
protected abstract readonly repository: Repository<T>;
@Get(':id')
async get(@Param('id', ParseIntPipe) id: number): Promise<T> {
return this.repository.findOneById(id);
}
@Get('list')
async list(): Promise<T[]> {
return this.repository.find();
}
@Post()
async post(@Body() dto: DeepPartial<T>): Promise<T> {
return this.repository.save(dto);
}
@Delete()
async delete(@Param('id', ParseIntPipe) id: number): Promise<void> {
this.repository.delete(id);
}
@Patch(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: QueryDeepPartialEntity<T>,
): Promise<void> {
this.repository.update(id, dto);
}
}
So yeah that is it, we now made our generic controller with all the CRUD endpoints. That is it for the first part, in the next part, I will do more work on the list function, in order to do pagination, ordering, filter.. etc. So follow for more ^^.
Top comments (3)
Wow. That was really good. I've used the
@nestjsx/crud
few times, always wondered how it was working under the hood, this kinda looks like a lite version of it.Thank you for this article. Waiting for part 2.
why u r using the repository directly in the controller instead of injecting a Service, and so u have to build a generic service that communicate with the database !
But swagger not working, not load body, if we use abstract controller