Understanding ManyToOne and OneToMany in Nest.js

Alec Mather
5 min readMar 30, 2024

Let’s create a very basic table of artists in the music industry, where each row represents a single artist.

Super simple.

Now let’s add a second table of songs. Each song is going to have a foreign key reference to an artist, but it will also have it’s own unique primary key (obviously).

This is a very basic and common setup when connecting tables in postgres.

Now let’s think about how to represent this relationship in Nest.js

We introduce this idea of something being “ManyToOne” or “OneToMany”, which I think causes confusion because we tend to associate the entire relationship as either being “ManyToOne” or “OneToMany”, but that’s actually not the correct way of thinking about it.

The correct way to think about it is, “this property is ManyToOne relative to that property.”

For example, in our tables, the artists.artist_id property has a OneToMany relationship with the songs.artist_id property.

However

If you describe it in the inverse, you would say that the songs.artist_id property has a ManyToOne relationship with the artists.artist_id property.

So here’s how we would set this up in Nest.js

This is the Nest.js equivalent of our postgres setup.

Now let’s create the Songs entity equivalent, and see how to set up a foreign key connection.

The only thing that’s different about this setup, is the second column, and this is how we define a foreign key in the Nest.js TypeORM.

The first thing we see is this “ManyToOne” annotation. Here’s how we would read this in plain english…

For any given unique key in the reference table (Artist in this case), there can be many instances of that key in this table.

The first parameter to this annotation is a function that returns the entity type of the object it is connecting to (again in this case, we’re connecting it to the Artist entity).

The second parameter is asking us: “which field on the reference table should we be the foreign key of?” In postgres, the equivalent is the part that says references artist(artist_id)

The JoinColumn({ name: 'artist_id' }) is defining the name of the foreign key field on the physical table in postgres.

The last part that says artist: Artist is a bit misleading because typically this would be where we would define the name & type of the postgres column, but actually we already defined the name of the column in the JoinColumn part, and we defined the type in the first parameter of the ManyToOne annotation.

So what is the purpose of artist: Artist; ?

This defines the object that gets connected as a result of a merge

So let’s say that we wanted to get all songs by a particular artist

select *
from songs s
where s.artist_id = 1

If we do this in TypeORM, it will return something like this…

[
{
song_id: 1,
name: "Song 1"
},
{
song_id: 2,
name: "Song 2"
}
]

But let’s say that we wanted to include the artist object inside each song object. Well, now we would need to “Merge the Artist entity into the Song entity.”

Which in the TypeORM, means that we have to “map the Artist object to a property on the Song entity.”

This is where artist: Artist; comes in in our entity definition.

So basically all we have to do is query our repository like this:

this.songRepository.findAll({
relations: ['artist']
})

And now our ORM is set up to map the result of the merge query to a single property that stores the nested Artist object. And the result will look like this.

[
{
song_id: 1,
name: "Song 1",
artist: {
artist_id: 1,
name: "Artist 1",
}
},
{
song_id: 2,
name: "Song 2",
artist: {
artist_id: 1,
name: "Artist 1",
}
}
]

In other words, if we had defined and queried it like this…

@Entity()
export class Artist {
@PrimaryGeneratedColumn()
artists_table_primary_artist_id: number;

@Column()
name: string;
}

@Entity()
export class Song {
@PrimaryGeneratedColumn()
the_songs_primary_song_id: number;

@ManyToOne(() => Artist, (artistTable) => artistTable.artists_table_primary_artist_id))
@JoinColumn({ name: 'songs_table_foreign_artist_id_name' })
songs_table_artist_object_property: Artist;

@Column()
name: string;
}
this.songRepository.findAll({
relations: ['songs_table_artist_object_property']
})

Then the result would be

[
{
the_songs_primary_song_id: 1,
name: "Song 1",
songs_table_artist_object_property: {
artists_table_primary_artist_id: 1,
name: "Artist 1",
}
},
{
the_songs_primary_song_id: 2,
name: "Song 2",
songs_table_artist_object_property: {
artists_table_primary_artist_id: 1,
name: "Artist 1",
}
}
]

Ok so at this point, there is a logical question to ask which is… what if we wanted to query for THIS data structure…

[
{
artist_id: 1,
name: "Artist 1",
songs: [
{
song_id: 1,
name: "Song 1"
},
{
song_id: 1,
name: "Song 1"
}
]
},
// ...more artist objects
]

Well, now we need to configure the Artist entity to know how to merge the Song entity onto it. So let’s go back to our Artist entity.

@Entity()
export class Artist {
@PrimaryGeneratedColumn()
artist_id: number;

@Column()
name: string;
}

In this table, there is only one row per unique artist_id which is the column doing the merging in this scenario. However, there can be many instances of the same artist_id in the table we’re merging to (i.e. the Song table).

So it might look something like this…

So in this scenario (i.e. when referencing the Songs table from the Artist entity) we would need a OneToMany connection, because there is only one row per unique artist_id that maps to many rows in the songs.artist_id foreign key.

@Entity()
export class Artist {
@PrimaryGeneratedColumn()
artist_id: number;

@OneToMany(() => Song, (song) => song.artist)
songs: Song[];

@Column()
name: string;
}

And now we can query the TypeORM like this

this.artistRepository.findAll({
relations: ['songs']
})

Which gives us this

[
{
artist_id: 1,
name: "Artist 1",
songs: [
{
song_id: 1,
name: "Song 1"
},
{
song_id: 1,
name: "Song 1"
}
]
},
// ...more artist objects
]

And here is the full analogous configuration of what we just set up…

--

--