Repository pattern — don't expose the ORM to services
Tomorrow you swap TypeORM for Prisma or vice versa — change the repository only, services don't move.
nestjs-data-repository-pattern
Why it matters
| Stake | If ignored |
|---|---|
| Hard to change |
|
| Hard to test |
|
| Layer mixing |
|
How to fix
A service does not receive Repository<Site> from TypeORM or PrismaClient. It receives a SitesRepository you wrote — a clear interface.
Examples
ts
@Injectable()
export class SitesService {
constructor(@InjectRepository(Site) private repo: Repository<Site>) {}
async getSite(id: string) {
return this.repo
.createQueryBuilder('site')
.leftJoinAndSelect('site.owner', 'owner')
.where('site.id = :id', { id })
.getOne();
// service knows about TypeORM QueryBuilder
}
}ts
// libs/sites-management/backend-data-access/src/lib/sites.repository.ts
export interface SitesRepository {
findById(id: string): Promise<Site | null>;
findByOwner(ownerId: string): Promise<Site[]>;
create(input: CreateSiteInput): Promise<Site>;
}
@Injectable()
export class TypeOrmSitesRepository implements SitesRepository {
constructor(@InjectRepository(SiteEntity) private repo: Repository<SiteEntity>) {}
async findById(id: string): Promise<Site | null> {
const entity = await this.repo.findOne({ where: { id }, relations: ['owner'] });
return entity ? toDomain(entity) : null;
}
}
// libs/sites-management/backend-feature/src/lib/sites.service.ts
@Injectable()
export class SitesService {
constructor(@Inject(SITES_REPOSITORY) private repo: SitesRepository) {}
async getSite(id: string) {
return this.repo.findById(id);
}
}