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.
