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
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.
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.
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.
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