RelationsTechniques · NestJS

Give cats owners: a one-to-many and its many-to-one, typed with Relation<> so the two entity files can import each other under ES modules, loaded on request and set by assignment.

What you will learn

Read the theory for Relations

All Techniques lessons

All NestJS courses

loading types…

What you'll learn

  • Declare a one-to-many relation and its inverse many-to-one, and say which side holds the foreign key
  • Explain why a relation property typed with the bare class can fail at load time under ES modules, and type it with Relation<>
  • Load a relation on request with relations: { ... }, and set one by assigning the entity and saving

Relations

A cat has an owner, an owner has cats. In an array that is a nested object; in a database it is two tables and a foreign key, cat.ownerId pointing at owner.id. TypeORM lets you keep writing cat.owner and owner.cats and works out the key, the join and the direction, provided you tell it which properties are related and how. This lesson adds owners to the cats API and, along the way, meets the one place where the docs' example fails under native ES modules and what TypeORM offers instead.

Three kinds of relation

TypeORM has @OneToOne(), @OneToMany() with its counterpart @ManyToOne(), and @ManyToMany(). The docs' example is the one-to-many, a user with photos:

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @OneToMany((type) => Photo, (photo) => photo.user)
  photos: Photo[];
}

Two arrows. The first, () => Photo, names the other entity lazily, as a function, because when this class is being defined Photo may not be defined yet: the two files import each other. The second, (photo) => photo.user, names the inverse side, the property on Photo that points back. The other side looks like this:

@Entity()
export class Photo {
  @ManyToOne(() => User, (user) => user.photos)
  user: User;
}

The @ManyToOne side owns the foreign key: a userId column appears on the photo table, and nothing appears on user. Options on the decorator control the rest: { nullable: true } allows a photo with no user, { onDelete: 'CASCADE' } deletes photos with their user, { eager: true } loads the relation with every query, { cascade: true } saves related objects when the owner is saved.

Loading a relation

By default a relation is not loaded: find() returns cats whose owner property is simply absent, because joining every table every time would be slow. Ask for it:

this.cats.find({ relations: { owner: true } });
this.owners.findOne({ where: { id }, relations: { cats: true } });

A loaded relation with no row behind it is null. Setting one is assigning the entity: cat.owner = owner; await this.cats.save(cat); writes the foreign key; cat.owner = null clears it.

The ESM trap, and Relation<>

Here is the docs' user: User under native ES modules, which is what this runtime and a Nest 12 project run. TypeScript emits the property's type as decorator metadata, design:type, evaluated when the class is defined. cat.entity.ts imports owner.entity.ts and vice versa, so whichever file loads second reads the other's class before its initialisation, and the boot dies with Cannot access 'Owner' before initialization. Which file loads second depends on who imported what first, so the bug appears and disappears with unrelated edits. Fundamentals lesson 6 met the same error on a constructor parameter.

TypeORM's own answer is the Relation<T> wrapper type:

@ManyToOne(() => Owner, (owner) => owner.cats)
owner: Relation<Owner>;

@OneToMany(() => Cat, (cat) => cat.owner)
cats: Relation<Cat[]>;

Relation<Owner> is Owner to the type checker, so nothing changes in your code, and it is a type alias, so the emitted metadata is Object and never touches the class at load time. The lazy () => Owner in the decorator is evaluated later, when TypeORM builds its metadata, by which time both files are loaded. Use Relation<> on every relation property, both sides.

Your task

Cats gain owners.

  1. owner.entity.ts: an owner has many cats, the inverse of Cat.owner, typed with Relation<>.
  2. cat.entity.ts: a cat belongs to at most one owner, nullable, inverse Owner.cats, typed with Relation<>.
  3. CatsService: findAll and findOne load the owner; findOwner loads the cats; adopt finds the owner (404 "Owner #9 not found" when missing), assigns it, saves, and returns the cat with its owner loaded.

Try step 2 with the docs' bare owner: Owner first and run. Depending on which file loads first you may see the boot fail with the before initialization error, and now you know why.

When it fails

  • Cannot access 'Owner' before initialization: a relation property is typed with the bare class. Use Relation<Owner>.
  • owner is missing from every cat: the relation exists but was not loaded; add relations: { owner: true }.
  • Entity "Owner" ... relation "cats" ... inverse side or similar metadata errors: the second arrow points at a property that does not exist on the other class, or is missing on one side.
  • POST /cats/1/adopt/1 answers 201 with owner: null: the owner was assigned but the response came from the object before the save, or findOne afterwards did not load the relation.

Remember

  • @OneToMany and @ManyToOne come in pairs; the many-to-one side holds the foreign key.
  • Both arrows are lazy: the entity, then the inverse property.
  • Relations load only when asked, with relations: { name: true }; unloaded is absent, loaded-but-empty is null.
  • Under ES modules, type relation properties with Relation<T>; the bare class breaks depending on load order.
Stuck? Show a hint

owner.entity.ts: @OneToMany(() => Cat, (cat) => cat.owner) cats: Relation<Cat[]>. cat.entity.ts: @ManyToOne(() => Owner, (owner) => owner.cats, { nullable: true }) owner: Relation<Owner> | null. Service: find({ relations: { owner: true }, order: { id: 'ASC' } }); findOne with the same relations; in adopt, findOneBy the owner, throw NotFoundException when null, cat.owner = owner, save, then return findOne(id); findOwner with relations: { cats: true }.