Master TypeORM ManyToMany Relation with NestJS and Postgres
Posted November 20, 2023
A TypeORM Many-to-Many relationship creates multiple records in one table but is associated with multiple records in another table. TypeORM will use an intermediary table (junction or link table) as a Many-to-Many relation.
This guide teaches how to perfectly implement a ManytoMany (M2M) Relationship with TypeORM using NestJS and PostgreSQL database. You’ll use the TypeORM @ManyToMany
decorator and query builder to model a Many-to-Many relationship between two entities.
You will learn:
- How to create two entities and add a Many-to-Many relationship with the TypeORM query builder.
Related: TypeORM OneToMany and ManyToOne with Nest.js and PostgreSQL
- Create a junction table using TypeORM
@JoinTable()
decorator. - Use PostgreSQL or MySQL to add your Many-to-Many entity relation.
- Updating and saving ManytoMany relations
- How to create a single POST method to load and apply TypeORM ManytoMany relations.
- Deleting Many-to-Many relations using cascades.
A Closer Look to TypeORM ManyToMany Relations
Many-to-many represents two or more entities. Each entity contains an element of one entity. The element in question is associated with multiple elements of another entity, and vice versa, to create a Many-to-Many relation. These multiple elements are then represented using a linking/junction table.
The linking table will now contain foreign key references to the primary keys of the two entities involved in your ManyToMany relationship.
Hypothetical TypeORM ManyToMany Relationship
Take this example: Students and Courses. Here, you will have Students and Courses entities as follows:
- Students
id (Primary Key)
StudentName
+-------------+--------------+----------------------------+
| Students |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| StudentName | varchar(255) | |
+-------------+--------------+----------------------------+
- Courses
id (Primary Key)
CourseName
+-------------+--------------+----------------------------+
| Courses |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| CourseName | varchar(255) | |
+-------------+--------------+----------------------------+
Up to this point, the two tables are independent and have no relation attached to them. This is where you will now create a Junction table linking the two with their respective primary keys acting as foreign key references.
Let’s name the linking table StudentCourses
that creates ManyToMany relation to Student
can have many Courses
and a Course
can belong to many Students
.
Based on this ManyToMany relation, the linking table will be represented as follows:
StudentID (Foreign Key referencing Students)
CourseID (Foreign Key referencing Courses)
+-------------+--------------+----------------------------+
| StudentCourse |
+-------------+--------------+----------------------------+
| StudentID | int(11) | PRIMARY KEY FOREIGN KEY |
| CourseID | int(11) | PRIMARY KEY FOREIGN KEY |
+-------------+--------------+----------------------------+
Your many-to-many relationship will be as follows:
Let’s now dive in and represent this setup and create a ManytoMany relationship using TypeORM, NestJS, and PostgreSQL.
Creating ManyToMany Relations Using TypeORM Entities
Before going further, ensure you have your NestJS ready using the following command:
npm i -g @nestjs/cli
nest new typeorm-manytomany
Navigate to the new typeorm-manytomany
directory and install the necessary dependencies:
cd typeorm-manytomany
npm install @nestjs/typeorm typeorm pg
Because we are using two Entities, we will represent each using a module as follows:
- Students
nest g module student
nest g service student --no-spec
nest g controller student --no-spec
- Courses
nest g module course
nest g service course --no-spec
nest g controller course --no-spec
Creating ManyToMany Relations Using TypeORM Entities
TypeORM uses Entities to represent an actual database using your application’s typescript code instead of SQL syntax. Let’s dive in and represent the two Entities (Students and Courses) and add a ManyToMany relationship within them.
Head to the src/student
directory and create a student.entity.ts
file. This will represent a Student database as follows:
// student.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Course } from '../course/course.entity';
@Entity()
export class Student {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
Likewise, go to the src/course
directory and create a course.entity.ts
file. This will represent a Course database as follows:
// course.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Student } from '../student/student.entity';
@Entity()
export class Course {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
Note that TypeORM uses decorators to represent your entities and fields as they should in your database. Now add the ManyToMany Relations to these two entities.
We will start with the Student entity and update the code as follows:
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Course } from '../course/course.entity';
@Entity()
export class Student {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Course, course => course.students)
@JoinTable()
courses: Course[];
}
Here:
- The
@ManyToMany
decorator defines the Many-to-Many relationship between Student and Course. @JoinTable()
creates a junction table. TypeORM will automatically create your junction table and add the relevant foreign key relationships and you don’t have to do that manually.
This relationship is not complete yet. Student must be related to Course as course => course.students
must point to the Course is relates to as follows:
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Student } from '../student/student.entity';
@Entity()
export class Course {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Student, student => student.courses)
students: Student[];
}
In this second setup, you only need to add your @ManyToMany
decorator and TypeORM will point to the right relationship. You don’t need @JoinTable()
as it’s already handled.
In most cases, @JoinTable()
should be added to the actor
. Here it is the Student that owns many courses but Courses do not own the Student. Basically, it is the student who enrolls in courses and not vice versa. The point is that @JoinTable
must only be on one side of the relation.
Note that relations can be uni-directional and bi-directional. Uni-directional uses the @ManyToMany
decorator only on one side. But bi-directional relations use @ManyToMany
decorators on both sides of a relation, just as we have in the above two entities.
How to Soft Delete a ManyToMany Relation with Cascade
When using TypeORM, you get the benefit of decorators. They handle the soft behaviors of your app without creating complicated functions.
As explained, @JoinTable
will create a linking table between the two entities we have. But the question is, what will happen to your joining table if you delete a student or a course?
Chances are, the table won’t change, although you have deleted the corresponding foreign keys.
To handle this, TypeORM uses cascade
to automatically propagate such operations (soft delete) in your entities. In this case, cascade
must be added to automatically remove related entities when the ownership entity is removed.
Here, you will use:
{ cascade: true }
This way, you’re instructing TypeORM to automatically remove the related records in the join table when entities are deleted. Update your @ManyToMany
decorators as follows:
- Student Entity:
// src/student/student.entity.ts
// .. other imports
export class Student {
// ... other code
// Add cascade: true as follows
@ManyToMany(() => Course, course => course.students, { cascade: true })
@JoinTable()
courses: Course[];
}
- Course Entity:
// src/course/course.entity.ts
// .. other imports
export class Student {
// ... other code
// Add cascade: true as follows
@ManyToMany(() => Student, student => student.courses, { cascade: true })
@JoinTable()
students: Student[];
}
Adding Data Using ManyToMany and TypeORM
Now you have the entities ready. You now need to create the modules and controllers needed to create HTTP methods and routes to execute and add TypeORM Many-to-Many data to the database.
First, you need to create Students and Courses. This way, you will be required to join the two and establish your relationship.
In your src/student/student.service.ts
file, add the following methods:
// student.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Student } from './student.entity';
@Injectable()
export class StudentService {
constructor(
@InjectRepository(Student)
private studentRepository: Repository<Student>,
) {}
async createStudent(studentData: Partial<Student>): Promise<Student> {
const student = this.studentRepository.create(studentData);
return this.studentRepository.save(student);
}
async getAllStudents(): Promise<Student[]> {
return this.studentRepository.find();
}
}
Here, we want to be able to create and fetch students. Your src/student/student.controller.ts
file will execute them using the HTTP methods and create the student endpoint as follows:
// student.controller.ts
import { Controller, Get, Param, Post, Body, Put, Delete, HttpException, HttpStatus } from '@nestjs/common';
import { StudentService } from './student.service';
import { Student } from './student.entity';
@Controller('students')
export class StudentController {
constructor(private readonly studentService: StudentService) {}
@Post()
async createStudent(@Body() studentData: Partial<Student>): Promise<Student> {
return this.studentService.createStudent(studentData);
}
@Get()
async getAllStudents(): Promise<Student[]> {
return this.studentService.getAllStudents();
}
}
You now need to update src/student/student.controller.ts
as follows:
// src/student/student.module.ts
import { Module } from '@nestjs/common';
import { StudentService } from './student.service';
import { StudentController } from './student.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Student } from './student.entity';
@Module({
imports: [
TypeOrmModule.forFeature([Student]), // Import the Student entity into the module
],
providers: [StudentService],
controllers: [StudentController]
})
export class StudentModule {}
Repeat the same process and update your course code base. To make it simple, we will only use the create method for adding courses (You can catch the whole code on GitHub) as follows:
src/course/course.service.ts
// course.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Course } from './course.entity';
@Injectable()
export class CourseService {
constructor(
@InjectRepository(Course)
private courseRepository: Repository<Course>,
) {}
async createCourse(courseData: Partial<Course>): Promise<Course> {
const course = this.courseRepository.create(courseData);
return this.courseRepository.save(course);
}
}
src/course/course.controller.ts
// course.controller.ts
import { Controller, Get, Param, Post, Body, Put, Delete } from '@nestjs/common';
import { CourseService } from './course.service';
import { Course } from './course.entity';
@Controller('courses')
export class CourseController {
constructor(private readonly courseService: CourseService) {}
@Post()
async createCourse(@Body() courseData: Partial<Course>): Promise<Course> {
return this.courseService.createCourse(courseData);
}
}
src/course/course.module.ts
import { Module } from '@nestjs/common';
import { CourseService } from './course.service';
import { CourseController } from './course.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Course } from './course.entity';
@Module({
imports: [
// Import the Course entity into the module `TypeOrmModule`
TypeOrmModule.forFeature([Course]),
],
providers: [CourseService],
controllers: [CourseController]
})
export class CourseModule {}
Loading ManyToMany Relations with TypeORM and PostgreSQL
TypeORM must establish a connection to your PostgreSQL database for this relation to work.
You can as well use SQLite, Microsoft SQL Server (MSSQL) or MySQL as you TypeORM database choice.
You will simply grab your database connection values while making sure you have an exacting database and update your src/app.module.ts
file as follows:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { StudentModule } from './student/student.module';
import { CourseModule } from './course/course.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
// Use TypeOrmModule and define the following database settings
imports: [TypeOrmModule.forRoot({
// The database TypreORM will communicate with
"type":"postgres",
// Where the database is hosted, local in this case
"host":"localhost",
// DB port
"port":5432,
// If you have a different db username, update it as such
"username":"postgres",
// add your db password
"password":"pass",
// Ensure you have the database created and add it here
"database":"school",
// Load entities. This should remain the same, and TypeORM will
// Load the entities and represent them in your database
"entities":[__dirname + "/**/**/**.entity{.ts,.js}"],
"synchronize":true
}),
StudentModule,
CourseModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Check this Create, Generate, and Run TypeORM Migrations in NestJS with PostgreSQL guide if you want to add migrations to your database.
Now Ensure your PostgreSQL is running and, execute the following command, and start your NestJS TypeORM server:
npm run start:dev
TypeORM will immediately create your table and establish ManyToMany using the Join:
You can now use Postman to test your app as follows:
To add Student: Send a POST request to http://localhost:3000/students
:
Adding Courses: Send a POST request to http://localhost:3000/courses
as follows:
These changes should be reflected in your database. Ensure to check that using GET requests.
Establishing ManyToMany Relationship Between Two Entities using TypeORM
Up to now, you are able to create students and courses. But we have no methods that create a student course relation. A student needs to enroll in an available course, and we need to use NestJS and TypeORM to get this done.
In that regard, you need to create methods and endpoints with the ManyToMany relation pointing in both directions.
You will create a PUT method that, when sent, the existing students will enroll in the available courses.
Go to your src/student/student.service.ts
file and add:
constructor
toCourseService
and anInjectRepository
toCourse
entity.- An
enrollStudentInCourses
method will load the student’s course relation. - Assign fetched courses to the student
- Save the updated student with the new Assigned course.
Here is a complete code sample that you need to do the above:
// student.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Student } from './student.entity';
import { CourseService } from '../course/course.service'; // Import CourseService
import { Course } from '../course/course.entity'; // Import Course entity
@Injectable()
export class StudentService {
constructor(
@InjectRepository(Student)
private studentRepository: Repository<Student>,
private courseService: CourseService,
@InjectRepository(Course)
private courseRepository: Repository<Course>, // Inject Course repository
) {}
async enrollStudentInCourses(studentId: number, courseIds: number[]): Promise<Student> {
const student = await this.studentRepository.findOne({
where: { id: studentId },
relations: ['courses'], // Load the courses relation for the student
});
if (!student) {
throw new Error('Student not found');
}
const courses = await this.courseRepository.findByIds(courseIds); // Fetch courses by IDs
if (!courses.length) {
throw new Error('No courses found with the provided IDs');
}
student.courses = courses; // Assign fetched courses to the student
return this.studentRepository.save(student); // Save the updated student
}
}
Go to your src/student/student.controller.ts
and create a PUT method to execute enrollStudentInCourses
. In this case, the endpoint must get the ID of the student to be updated and send a PUT request as follows:
@Put(':id/courses')
async enrollStudentInCourses(
@Param('id') id: string,
@Body() body: { courses: number[] },
): Promise<any> {
const studentId = +id;
const courseIds = body.courses;
try {
const updatedStudent = await this.studentService.enrollStudentInCourses(studentId, courseIds);
return { message: 'Student enrolled in courses successfully', data: updatedStudent };
} catch (error) {
if (error.status === HttpStatus.NOT_FOUND) {
throw new HttpException('Student not found', HttpStatus.NOT_FOUND);
}
throw new HttpException('Error enrolling student in courses', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Because you have used the CourseService
within the StudentService
, You will be required to update src/student/student.module.ts
as such:
// ... other imports
import { CourseModule } from './course/course.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
// db setting here
}),
StudentModule,
CourseModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Let’s now test if these work as expected. Send a PUT request to http://localhost:3000/students/1/courses
. In this case, 1
is the ID of the student you want to add Course relation to.
Your JSON payload will contain an array of the course IDs that you want Student 1
to have as follows:
{
"courses": [1,2,3]
}
Check your Join table student_courses_course
, and now your ManyToMany relations should be well structured within your linking table as follows:
You can see a student with ID 1
has courses 1 and 7
. Student id 4
has courses 1, 2, and 3
validating your ManyToMany relations are working fine.
Establishing ManyToMany Relation with POST Request
You have course and student ready. This means it’s not a must for you to send a PUT request, as we have done above. You can create a Student POST request if you have courses already available in your database table.
This way, you will only need to create new students and, at the same time, enroll them in the available courses with your ManyToMany relation remaining valid.
You will be required to create a new function, createStudentWithCourses
in your src/student/student.service.ts
file as follows:
async createStudentWithCourses(name: string, courseIds: number[]): Promise<Student> {
const student = this.studentRepository.create({ name });
try {
const courses = await this.courseRepository.findByIds(courseIds);
if (courses.length !== courseIds.length) {
throw new Error('One or more courses not found');
}
student.courses = courses;
return this.studentRepository.save(student);
} catch (error) {
throw new Error(`Error creating student: ${error.message}`);
}
}
In your src/student/student.controller.ts
, update the POST request as follows:
@Post()
async createStudentWithCourses(@Body() body: { name: string, courses: number[] }): Promise<any> {
const { name, courses } = body;
return this.studentService.createStudentWithCourses(name, courses);
}
Go to your Postman and send a POST request to http://localhost:3000/students
with the following JSON payload:
{
"name": "Marceline Avila",
"courses": [1,2] // Array of course IDs the student is enrolled in
}
You can see that the add Marceline Avila
student was assigned ID 7
and enrolled in courses with IDs I and 2
. Let’s check the linking table and verify that:
Indeed, we now have ManyToMany relations perfectly working courtesy of TypeORM and PostgreSQL.
Conclusion
This guide helped you learn and implement ManyToMany relations in NestJS using TypeORM. You can now
- Create entities and add a Many-to-Many relationship to them.
- Use TypeORM to create a joint Many-to-Many table.
- Update and create relations to your joining table.
Check the whole code in GitHub repo. I hope you found the guide helpful!