NestJS course Β· Module 3: TypeORM and Databases
Repository Pattern - organising the treasury
In this lesson6
You already know entities and relations - the treasury has its shelves catalogued and it is clear what connects to what. But who actually reaches into it? If every service in the Empire wrote its own SQL, the same "find a legionary by name" operation would exist in five places, in five versions, with five different bugs.
The Romans solved this differently: the treasury is reached through an archivist. A service does not descend into the cellar with a torch - it tells the archivist what it needs, and he knows which shelf holds it. In code, that archivist is a repository: the layer that separates business logic from the way data is stored.
Three steps to a repository
TypeORM gives you a repository for free for every entity - you just have to ask. The path is always the same, and it is worth remembering as a sequence.
Step one: the entity. You met it in previous lessons - a class with the @Entity() decorator describing a shelf in the treasury. Without it there is nothing to archive.
Step two: registration in the module. We tell NestJS which entities to prepare archivists for:
1// legionary.module.ts
2@Module({
3 imports: [TypeOrmModule.forFeature([Legionary])],
4 providers: [LegionaryService],
5 controllers: [LegionaryController],
6})
7export class LegionaryModule {}Note the name: forFeature, not forRoot. This difference is a frequent source of confusion, so let's separate them once and for all. You call forRoot once, in the root module - it configures the database connection for the whole application. You call forFeature in each module separately, listing only the entities that module actually uses. One sets the road to the treasury, the other assigns archivists to a particular cohort.
Step three: injection into the service. We receive the registered archivist in the constructor:
1@Injectable()
2export class LegionaryService {
3 constructor(
4 @InjectRepository(Legionary)
5 private legionaryRepo: Repository<Legionary>,
6 ) {}
7}The @InjectRepository(Legionary) decorator tells NestJS which archivist we are asking for - because an application has many, one per entity. The type Repository<Legionary> is generic notation: Repository is the general kind of archivist, and <Legionary> narrows it to this one entity. Thanks to that, TypeScript knows find() will return legionaries rather than just anything - and will suggest their fields as you type.
Remember the order: entity, forFeature in the module, @InjectRepository in the service. Skipping the middle step produces an unknown-provider error at application startup - and it is the most common cause of that message.
Reading - find with options
The archivist takes orders in the form of an options object. Its most important key is where:
1async findAll(): Promise<Legionary[]> {
2 return this.legionaryRepo.find();
3}
4
5async findCenturions(): Promise<Legionary[]> {
6 return this.legionaryRepo.find({
7 where: { rank: 'Centurion' },
8 relations: ['centurion'],
9 order: { name: 'ASC' },
10 });
11}find() with no arguments brings everything. With an options object - it narrows. Beware of one detail many people trip on: conditions must sit inside the where key. Writing find({ rank: 'Centurion' }) is not a filter - it is an unknown option which TypeORM ignores, and you will get every legionary instead of just the centurions. No error is raised, so it is easy to miss.
The relations key pulls in related entities - the repository's equivalent of leftJoinAndSelect from the Query Builder. order sorts. When you need a single record, you call findOne({ where: { id } }) - it returns the entity, or null when nothing matches.
Writing - create and save
Creating a new legionary is a pair of methods, and it is worth understanding why two rather than one:
1async create(dto: CreateLegionaryDto): Promise<Legionary> {
2 const legionary = this.legionaryRepo.create(dto);
3 return this.legionaryRepo.save(legionary);
4}create() does not touch the database. It only builds an entity object in memory - copying the fields from the DTO and giving it the Legionary class, so that decorators, hooks and default values will work. Only save() sends it to the treasury and returns the saved entity, now carrying the id assigned by the database.
This split has a practical point: between create and save you can still change or check something. save() also has a second face - called on an entity that already has an id, it performs an update instead of an insert. One method, two behaviours, depending on whether the record exists.
Updating - preload
For changing an existing record there is preload, a method with a non-obvious name:
1async update(id: number, dto: UpdateLegionaryDto): Promise<Legionary> {
2 const legionary = await this.legionaryRepo.preload({ id, ...dto });
3
4 if (!legionary) {
5 throw new NotFoundException(`Legionary ${id} does not exist`);
6 }
7
8 return this.legionaryRepo.save(legionary);
9}preload fetches the record with the given id from the database, lays the DTO's fields over it and returns an entity ready to be saved - but saves nothing yet. Its advantage is what you do not have to do: fields absent from the DTO keep their current values, so a partial update will not wipe the rest of the record. When no record with that id exists, preload returns undefined - hence the check before saving. Only save() makes the change permanent.
Deleting - remove and delete
Finally, two deletion methods which do the same thing in different ways:
1async remove(id: number): Promise<void> {
2 const legionary = await this.legionaryRepo.findOne({ where: { id } });
3
4 if (!legionary) {
5 throw new NotFoundException(`Legionary ${id} does not exist`);
6 }
7
8 await this.legionaryRepo.remove(legionary);
9}remove() takes an entity - which is why we fetch it first. That costs an extra query, but in exchange you get certainty that the record existed, and you trigger the deletion hooks. delete(id) takes the bare id and erases it in a single query - faster, but with no check and no hooks. Choose deliberately: remove where correctness and reacting to a missing record matter, delete where speed does.
Summary
The treasury has an archivist and nobody goes down to the cellar on their own:
- a repository separates business logic from the way data is stored,
- the path is always three steps: an entity with
@Entity(),TypeOrmModule.forFeature([Entity])in the module,@InjectRepository(Entity)in the service constructor, forRootconfigures the connection once for the application,forFeatureassigns entities to a single module,find()takes thewhere,relationsandorderoptions - conditions outside thewherekey are silently ignored,create()builds an entity in memory,save()writes it to the database and can update too,preload()merges an existing record with your changes without saving - it returnsundefinedwhen the record is absent,remove(entity)deletes more safely and with hooks,delete(id)faster and without them.
In the next lesson you will meet the Query Builder - the tool for questions the archivist cannot handle with find() alone. For now remember: a repository is the treasury's archivist - the service says what it needs, not how to take it off the shelf.
Code for this lesson: src/repository-pattern.ts
1// Repository Pattern - Treasury Organization
2import { Injectable, NotFoundException } from '@nestjs/common';
3import { InjectRepository } from '@nestjs/typeorm';
4import { Repository } from 'typeorm';
5
6console.log("Repository Pattern - the manager of data access!");
7
8// ===========================================
9// 1. Basic service with a repository
10// ===========================================
11
12// We assume the Legionary entity from the previous lesson
13
14@Injectable()
15export class LegionaryService {
16 constructor(
17 @InjectRepository('Legionary')
18 private legionaryRepo: Repository<any>,
19 ) {}
20
21 // Get all
22 async findAll() {
23 return this.legionaryRepo.find();
24 }
25
26 // Get by ID
27 async findById(id: number) {
28 const legionary = await this.legionaryRepo.findOneBy({ id });
29 if (!legionary) {
30 throw new NotFoundException('Legionary non inventus!');
31 }
32 return legionary;
33 }
34
35 // Create a new one
36 async create(data: any) {
37 const legionary = this.legionaryRepo.create(data);
38 return this.legionaryRepo.save(legionary);
39 }
40
41 // Update
42 async update(id: number, data: any) {
43 const legionary = await this.findById(id);
44 Object.assign(legionary, data);
45 return this.legionaryRepo.save(legionary);
46 }
47
48 // Delete
49 async remove(id: number) {
50 const result = await this.legionaryRepo.delete(id);
51 if (result.affected === 0) {
52 throw new NotFoundException('Legionary non inventus!');
53 }
54 }
55
56 // Find by rank
57 async findByRank(rank: string) {
58 return this.legionaryRepo.find({ where: { rank } });
59 }
60
61 // Count legionaries in the legion
62 async countByLegion(legionName: string) {
63 return this.legionaryRepo.count({
64 where: { legion: legionName },
65 });
66 }
67}
68
69// ===========================================
70// 2. CRUD service with the garrison
71// ===========================================
72
73@Injectable()
74export class GarrisonService {
75 constructor(
76 @InjectRepository('Garrison')
77 private garrisonRepo: Repository<any>,
78 ) {}
79
80 async findAll() {
81 return this.garrisonRepo.find({
82 order: { soldiers: 'DESC' },
83 });
84 }
85
86 async create(data: any) {
87 return this.garrisonRepo.save(data);
88 }
89
90 async update(id: number, data: any) {
91 await this.garrisonRepo.update(id, data);
92 return this.garrisonRepo.findOneBy({ id });
93 }
94
95 async remove(id: number) {
96 await this.garrisonRepo.delete(id);
97 }
98}
99
100// ===========================================
101// Registration in the module
102// ===========================================
103
104// @Module({
105// imports: [TypeOrmModule.forFeature([Legionary, Garrison])],
106// providers: [LegionaryService, GarrisonService],
107// exports: [LegionaryService],
108// })
109// export class LegionaryModule {}
110
111console.log("\n=== REPOSITORY SUMMARY ===");
112console.log("@InjectRepository(Entity) - inject the repository");
113console.log("repo.find() - fetch all records");
114console.log("repo.findOneBy() - fetch one record");
115console.log("repo.save() - save (insert or update)");
116console.log("repo.delete() - delete record");
117console.log("repo.count() - count records");
118Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. How do you register an entity repository in a NestJS module?
2. How do you find all legionaries with the rank 'Centurion' using a TypeORM repository?
Hands-on tasks in the game
- Vertical ordering
Arrange the steps for configuring a TypeORM repository in a NestJS module:
- Code editor
Write a LegionaryService with @InjectRepository(Legionary) and methods findAll(), findOne(id), create(dto) and remove(id)
- Horizontal ordering
Arrange the elements of repository injection in the service constructor:
- Click in order
Arrange the elements of saving a new entity via a repository:
- Code editor
Write a service with methods: find() with where and relations, create() with save, update() with preload and save, delete() with remove