NestJS Relations

Relations connect two database tables together. A user has many orders. An order belongs to one user. A post has many tags, and each tag can belong to many posts. TypeORM lets you define these connections directly in your entity classes using relation decorators, then load related data with simple options instead of writing SQL JOIN statements.

The Four Relation Types

Relation        | Example
----------------|-------------------------------------------
One-to-One      | One user has one profile
One-to-Many     | One user has many orders
Many-to-One     | Many orders belong to one user
Many-to-Many    | Many posts share many tags

One-to-One

One user has exactly one profile. One profile belongs to exactly one user.

// profile.entity.ts
@Entity()
export class Profile {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  bio: string;

  @OneToOne(() => User, (user) => user.profile)
  user: User;
}

// user.entity.ts
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToOne(() => Profile, (profile) => profile.user, { cascade: true })
  @JoinColumn()   ← adds the foreign key column to users table
  profile: Profile;
}
Database:
users table         profiles table
┌────┬──────────────────┐   ┌────┬─────────────┬──────────┐
│ id │ profile_id (FK)  │   │ id │ bio         │          │
├────┼──────────────────┤   ├────┼─────────────┼──────────┤
│ 1  │ 1                │   │ 1  │ "Developer" │          │
└────┴──────────────────┘   └────┴─────────────┴──────────┘

One-to-Many and Many-to-One

One user places many orders. Each order belongs to one user. These two decorators always work as a pair on the two sides of the same relationship.

// user.entity.ts
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @OneToMany(() => Order, (order) => order.user)
  orders: Order[];
}

// order.entity.ts
@Entity()
export class Order {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  total: number;

  @ManyToOne(() => User, (user) => user.orders)
  user: User;    // foreign key column 'userId' added to orders table
}
Database:
users table          orders table
┌────┬──────┐        ┌────┬───────┬─────────────┐
│ id │ name │        │ id │ total │ userId (FK) │
├────┼──────┤        ├────┼───────┼─────────────┤
│ 1  │ Alice│        │ 1  │ 250   │ 1           │
└────┴──────┘        │ 2  │ 90    │ 1           │
                     └────┴───────┴─────────────┘

Many-to-Many

Many posts can have many tags. Many tags can appear in many posts. TypeORM creates a junction table automatically to store these connections.

// post.entity.ts
@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @ManyToMany(() => Tag, (tag) => tag.posts)
  @JoinTable()    ← creates the junction table (place on owning side)
  tags: Tag[];
}

// tag.entity.ts
@Entity()
export class Tag {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => Post, (post) => post.tags)
  posts: Post[];
}
Database:
posts table   posts_tags_tags (junction)   tags table
┌────┬──────┐ ┌──────────┬────────┐        ┌────┬──────────┐
│ id │ title│ │ postsId  │ tagsId │        │ id │ name     │
├────┼──────┤ ├──────────┼────────┤        ├────┼──────────┤
│ 1  │ NestJ│ │ 1        │ 1      │        │ 1  │ backend  │
│ 2  │ React│ │ 1        │ 2      │        │ 2  │javascript│
└────┴──────┘ │ 2        │ 2      │        └────┴──────────┘
              └──────────┴────────┘

Loading Related Data

Relations do not load automatically — you must request them explicitly to avoid unnecessary database queries:

// Load user with their orders
const user = await this.userRepository.findOne({
  where: { id: 1 },
  relations: ['orders'],
});
// user.orders = [ { id: 1, total: 250 }, { id: 2, total: 90 } ]

// Load user, their orders, and each order's items
const user = await this.userRepository.findOne({
  where: { id: 1 },
  relations: ['orders', 'orders.items'],
});

Cascade Operations

Cascade tells TypeORM to automatically perform operations on related entities when you perform them on the parent:

@OneToMany(() => Order, (order) => order.user, { cascade: true })
orders: Order[];

// Now saving a user also saves all their orders automatically
await this.userRepository.save(user);
Cascade options:
{ cascade: true }          → insert, update, remove, soft-remove, recover
{ cascade: ['insert'] }    → only on insert
{ cascade: ['remove'] }    → only on delete

Eager Loading

Mark a relation as eager to load it automatically on every find operation — no need to specify it in the relations option:

@ManyToOne(() => User, (user) => user.orders, { eager: true })
user: User;

Use eager loading sparingly. Loading too many relations automatically leads to slow queries and excessive data transfer, especially when you only need the parent record without the related data.

Relations connect your data model accurately to how data relates in the real world. Defining them in TypeORM entities gives you type-safe access to related records with minimal code and no manual SQL JOIN writing.

Leave a Comment

Your email address will not be published. Required fields are marked *