Master TypeORM ManyToMany Relation with NestJS and Postgres

Posted November 20, 2023
Master TypeORM ManyToMany Relation with NestJS and Postgres

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:

Master TypeORM ManyToMany Relation with NestJS and Postgres

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

Master TypeORM ManyToMany Relation with NestJS and Postgres

TypeORM will immediately create your table and establish ManyToMany using the Join:

Master TypeORM ManyToMany Relation with NestJS and Postgres

You can now use Postman to test your app as follows:

To add Student: Send a POST request to http://localhost:3000/students:

Master TypeORM ManyToMany Relation with NestJS and Postgres

Adding Courses: Send a POST request to http://localhost:3000/courses as follows:

Master TypeORM ManyToMany Relation with NestJS and Postgres

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 to CourseService and an InjectRepository to Course 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]
}

Master TypeORM ManyToMany Relation with NestJS and Postgres

Check your Join table student_courses_course, and now your ManyToMany relations should be well structured within your linking table as follows:

Master TypeORM ManyToMany Relation with NestJS and Postgres

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
}

Master TypeORM ManyToMany Relation with NestJS and Postgres

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:

Master TypeORM ManyToMany Relation with NestJS and Postgres

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!

Master TypeORM ManyToMany Relation with NestJS and Postgres

Written By:

Joseph Chege