Most of my NestJs controllers look the same. They have basic CRUD functionality and do the exact same things.
The only differences between the controllers are:
- the path
- the service that is injected (and the services are all extended from an abstract service)
- the entity that is returned from the methods
- the create, update, and query dtos
Here is an example CRUD controller:
@UseGuards(JwtAuthGuard)
@Controller("/api/warehouse/goods-receipts")
export class GoodsReceiptsController
implements ICrudController<GoodsReceipt, CreateGoodsReceiptDto, UpdateGoodsReceiptDto, QueryGoodsReceiptDto> {
constructor(private service: GoodsReceiptsService) {
}
@Post()
create(@Body() body: CreateGoodsReceiptDto, @CurrentUser() user: Partial<User>): Promise<GoodsReceipt> {
return this.service.createItem(body, user);
}
@Delete(":id")
delete(@Param() params: NumberIdDto): Promise<Partial<GoodsReceipt>> {
return this.service.deleteItem(params.id);
}
@Get(":id")
getOne(@Param() params: NumberIdDto): Promise<GoodsReceipt> {
return this.service.getItem(params.id);
}
@Get()
get(@Query() query: QueryGoodsReceiptDto): Promise<GoodsReceipt[]> {
return this.service.getItems(query);
}
@Patch()
update(@Body() body: UpdateGoodsReceiptDto, @CurrentUser() user: Partial<User>): Promise<GoodsReceipt> {
return this.service.updateItem(body,user);
}
}
This is the interface I have created for my controllers:
export interface ICrudController<EntityType, CreateDto, UpdateDto, QueryDto> {
getOne(id: NumberIdDto): Promise<EntityType>;
get(query: QueryDto): Promise<EntityType[]>;
create(body: CreateDto, user: Partial<User>): Promise<EntityType>;
update(body: UpdateDto, user: Partial<User>): Promise<EntityType>;
delete(id: NumberIdDto): Promise<Partial<EntityType>>;
}
Writing all these repetitive controllers has got pretty tiresome (yes I know about nest g resource but that is not really the point of this question), so I decided to create an abstract controller that will do most of the heavy lifting and have the controllers extend this.
export abstract class CrudController<T, C, U, Q> implements ICrudController<T, C, U, Q> {
protected service: ICrudService<T, C, U, Q>;
@Post()
create(@Body() body: C, @CurrentUser() user: Partial<User>): Promise<T> {
return this.service.createItem(body, user);
}
@Get(":id")
getOne(@Param() params: NumberIdDto): Promise<T> {
return this.service.getItem(params.id);
}
@Get()
get(@Query() query: Q): Promise<T[]> {
return this.service.getItems(query);
}
@Delete(":id")
delete(@Param() params: NumberIdDto): Promise<Partial<T>> {
return this.service.deleteItem(params.id);
}
@Patch()
update(@Body() body: U, @CurrentUser() user: Partial<User>): Promise<T> {
return this.service.updateItem(body, user);
}
}
Now all I need to do to add a new controller is this:
@UseGuards(JwtAuthGuard)
@Controller("/api/warehouse/goods-receipts")
export class GoodsReceiptsController
extends CrudController<GoodsReceipt, CreateGoodsReceiptDto, UpdateGoodsReceiptDto, QueryGoodsReceiptDto> {
constructor(protected service: GoodsReceiptsService) {
super();
}
}
I was very proud of myself at that point. That is until I figured out that validation no longer works because class-validator doesn't work with generic types.
There has to be some way I could fix this with minimal intervention and maximal use of reusable code?