Generic Services & Repositories

Remove CRUD boilerplate with an abstract generic base service, without sacrificing type-safety.

By the fifth entity, every service has the same findAll, findOne, create, remove. TypeScript generics let you write that once in an abstract base and specialize per entity - DRY without giving up types.

An abstract base service

Parameterize the base over the entity type T and its create DTO. Concrete services extend it, passing the repository up the chain. Shared behavior (pagination, soft-delete, auditing) lives in one place.

export abstract class BaseService<T> {
  protected constructor(protected readonly repo: Repo<T>) {}
  findAll() { return this.repo.find(); }
  findOne(id: string) { return this.repo.findById(id); }
}

The DI wrinkle

Generics are erased at runtime, so Nest can't inject "the repository for T" from the type alone. You still register a concrete repository provider per entity and pass it into super(). The generics buy you compile-time safety; DI stays concrete.

Constrain your type parameters

Use <T extends { id: string }> so the base can safely assume an id. Constraints turn the generic from "anything" into "anything that fits this contract," which is where the type-safety pays off.

Know when to stop

Generics shine for genuinely uniform CRUD. The moment an entity needs bespoke logic, override the method or drop the base entirely for that service. A generic base that every subclass fights against is worse than a little duplication.

Example

Example · typescript
import { Injectable, NotFoundException } from '@nestjs/common';

// A minimal repository contract the base can rely on.
export interface Repository<T> {
  find(): Promise<T[]>;
  findById(id: string): Promise<T | null>;
  save(entity: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

// Reusable CRUD, written once. Constrained to entities with an id.
export abstract class BaseService<T extends { id: string }> {
  protected constructor(protected readonly repo: Repository<T>) {}

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

  async findOne(id: string): Promise<T> {
    const found = await this.repo.findById(id);
    if (!found) throw new NotFoundException(`${id} not found`);
    return found;
  }

  create(dto: Partial<T>): Promise<T> {
    return this.repo.save(dto);
  }

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

interface User {
  id: string;
  email: string;
}

// Concrete service: DI stays explicit, types stay tight.
@Injectable()
export class UsersService extends BaseService<User> {
  constructor(private readonly usersRepo: Repository<User>) {
    super(usersRepo);
  }

  // Add only what's genuinely user-specific.
  findByEmail(email: string) {
    return this.usersRepo
      .find()
      .then((all) => all.find((u) => u.email === email) ?? null);
  }
}

When to use it

  • A developer writes an abstract AbstractCrudService<T, CreateDto, UpdateDto> so UsersService, ProductsService, and OrdersService all inherit standard CRUD without repeating code.
  • A team creates a generic repository base class typed to the entity so every concrete repository gets findById, findAll, create, and remove for free.
  • An engineer uses TypeScript generics on the base service to ensure that compile-time errors surface when a concrete service returns the wrong entity type.

More examples

Abstract generic base service

Defines an abstract base class generic over an entity type T, implementing findAll, findOne, create, and remove so concrete services inherit them.

Example · ts
import { NotFoundException } from '@nestjs/common';
import { Repository } from 'typeorm';

export abstract class AbstractCrudService<T extends { id: number }> {
  constructor(protected readonly repo: Repository<T>) {}

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

  async findOne(id: number): Promise<T> {
    const entity = await this.repo.findOneBy({ id } as any);
    if (!entity) throw new NotFoundException(`Entity ${id} not found`);
    return entity;
  }

  create(data: Partial<T>): Promise<T> { return this.repo.save(data as T); }

  async remove(id: number): Promise<void> {
    const entity = await this.findOne(id);
    await this.repo.remove(entity);
  }
}

Concrete service extending base

Extends AbstractCrudService with the Product entity so ProductsService inherits all CRUD methods and can add domain-specific queries.

Example · ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';
import { AbstractCrudService } from '../common/abstract-crud.service';

@Injectable()
export class ProductsService extends AbstractCrudService<Product> {
  constructor(
    @InjectRepository(Product) repo: Repository<Product>,
  ) {
    super(repo);
  }

  // Add domain-specific methods here
  findFeatured() {
    return this.repo.findBy({ featured: true });
  }
}

Generic controller mixin

Creates a mixin controller factory so concrete controllers inherit the standard GET/DELETE handlers from the generic base without repeating them.

Example · ts
import { Get, Post, Patch, Delete, Param, Body, ParseIntPipe } from '@nestjs/common';
import { AbstractCrudService } from './abstract-crud.service';

export function createCrudController<T extends { id: number }>(entity: new () => T) {
  class CrudController {
    constructor(protected readonly service: AbstractCrudService<T>) {}
    @Get()         findAll() { return this.service.findAll(); }
    @Get(':id')    findOne(@Param('id', ParseIntPipe) id: number) { return this.service.findOne(id); }
    @Delete(':id') remove(@Param('id', ParseIntPipe) id: number) { return this.service.remove(id); }
  }
  return CrudController;
}

Discussion

  • Be the first to comment on this lesson.