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.
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.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.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.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.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, @name: remove where correctness and reacting to a missing record matter, delete where speed does.The treasury has an archivist and nobody goes down to the cellar on their own:
@Entity(), TypeOrmModule.forFeature([Entity]) in the module, @InjectRepository(Entity) in the service constructor,forRoot configures the connection once for the application, forFeature assigns entities to a single module,find() takes the where, relations and order options - conditions outside the where key 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 returns undefined when 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.