DEV Community

Cover image for Building Extendable Generic Nestjs Controller
Amro
Amro

Posted on • Updated on

Building Extendable Generic Nestjs Controller

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;
}
Enter fullscreen mode Exit fullscreen mode

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 } });
  }
}
Enter fullscreen mode Exit fullscreen mode

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 ^^
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();}
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

Image description
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)

Collapse
 
premdasvm profile image
Premdas Vm

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.

Collapse
 
kassimovic profile image
kassem hassan moughraby

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 !

Collapse
 
muralitharan805 profile image
Muralitharan

But swagger not working, not load body, if we use abstract controller