(NestJS-5)Understanding TypeORM Entities with Relationships in NestJS: An E-commerce Example

Bhargava Chary
13 min readJun 27, 2024

--

In this article, we will delve into the concept of entities and their relationships in NestJS using TypeORM. We will use an e-commerce application as our example to illustrate these concepts. Understanding how to properly define and manage these relationships is crucial for building robust and scalable applications.

We will cover the basics of entities, different types of relationships, and how to implement them in a NestJS application. This article is mainly focused on beginners and it will be lengthy due to the depth of the content.

In previous article we have been covered who t configure different loggers in NestJS Application. If you are interested to config logger please refer to this.

Table of Contents

  1. Prerequisites
  2. What are Entities
  3. Understanding Relationships
  4. Problem Statement: E-commerce Relationships
  5. Generate Modules and Entities
  6. Defining Entities
  7. Class Variables and Their Treatment
  8. Setting Up Entities in TypeOrm Module
  9. Execution and Output
  10. Tips and Best Practices
  11. Conclusion

Prerequisites

Before we start, make sure you have the following:

  1. NestJS Project Setup: A basic NestJS project. If you don’t have one, you can refer here.
  2. TypeORM and MySQL: Installed and configured in your project, refer here.
  3. Basic Knowledge of SQL and Database Relationships: Understanding of basic SQL and how relational databases work.

What are Entities

Entities are the building blocks of your database schema. Simply put, an entity object represents a database table, and we define entities to interact with the database. Entities are written as simple classes, and we can use some OOP programming concepts like abstraction and inheritance.

Advantages of using entities

  • Provides a way to abstract database tables into TypeScript classes, making it easier to work with databases using an object-oriented approach.
  • Ensures data integrity and consistency through different decorators.
  • Allows you to define relationships between different tables (e.g., one-to-many, many-to-many).
  • Simplifies database queries and operations.

TypeORM provides an @Entity decorator to define an entity, and some decorators to define properties of data table columns like @PrimaryColumn, @Column, and so on.

Key Features of an Entities

  • Entities: Represent database tables as classes.
  • Decorators: Used to define mappings (@Entity(), @Column(), etc.).
  • Relationships: Define relationships using decorators like @OneToMany, @ManyToOne, etc.
  • OOP Features: Support inheritance and abstraction.
  • Repositories: Used to manage entity instances and perform database operations.

Understanding Relationships

In relational databases, entities often relate to each other in various ways. TypeORM supports several types of relationships:

  • One-to-One: Indicates that a single entity instance is related to a single instance of another entity.
  • One-to-Many / Many-to-One: One-to-Many is where a single entity instance is related to multiple instances of another entity. Conversely, many-to-one is where multiple instances of one entity relate to a single instance of another entity.
  • Many-to-Many: Indicates that multiple instances of an entity relate to multiple instances of another entity. This type of relationship requires a join table.

Relationships can be defined in two ways:

  • Unidirectional Relationships: Only one entity knows about the relationship.
  • Bidirectional Relationships: Both entities know about the relationship

Based on requirement, these relationships are defined.

Problem Statement: E-commerce Relationships

As stated in the title, we will work on an e-commerce example. In an e-commerce application, we typically have entities like User, Profile, Order, Product, and Category. The relationships among these entities might include:

  • A User can have one Profile (One-to-One).
  • A User can place many Orders, and many Orders can belong to a User (One-to-Many / Many-to-One).
  • An Order can include many Products and a Product can be part of many Orders (Many-to-Many).
  • A Product belongs to a Category and a Category can have many Products (Many-to-One / One-to-Many).

Generate Modules and Entities

As discussed in the problem statement, we now require the following modules in our application to configure entities and their relationships: User, Order, Product, and Category. If a module already exists, continue with the existing entities. If a module does not exist, create the module using the CRUD generator cli command as shown below, replace the module name with your desirable name.

$ nest g resource <module-name>

Select the REST API as the transport layer and enter Y or YES. It generates CRUD entry points, then click Enter.

Defining Entities

To define an entity in TypeORM, you use the @Entity() decorator on a class. Each property in the class is decorated with column-specific decorators such as @PrimaryGeneratedColumn(), @Column(), @CreateDateColumn(), and others.

Key Decorators

  • @Entity(): Marks the class as a database entity.
  • @PrimaryColumn(): This decorator is used to mark a property as the primary key . Additional options can be passed to generated IDs automatically like UUID, rowid, soon .
  • @Column(): This decorator is used to mark a property as a column in the database. Additional options can be passed to customize the column (e.g., select: false to exclude the column from SELECT queries by default).
  • @CreateDateColumn(): This decorator is used to mark a property as a column that will store the creation timestamp. The value is automatically managed by TypeORM.
  • @UpdateDateColumn(): This decorator is used to mark a property as a column that will store the last update timestamp. Like @CreateDateColumn(), the value is automatically managed by TypeORM.

User Entity and Profile Entity (One-to-One)

Create a profile.entity.ts file in the user > entities.

Note:- Try to use same relationships in all entities for now.

  • Unidirectional relationships:
// user.entity.ts
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';

@Entity()
export class User {
@PrimaryColumn({ generated: "uuid" }) // generated specifies if this column will use auto increment (sequence, generated identity, rowid).
user_id: string;

@Column()
name: string;

@Column()
email: string;

/*
* Select indicates row selection in QueryBuilder
* Default value is "true".
*/
@Column({ select: false })
password: string;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */
}

// profile.entity.ts
import { Entity, Column, PrimaryColumn, OneToOne, JoinColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
import { User } from './user.entity';

@Entity()
export class Profile {
@PrimaryColumn({ generated: "uuid" })
profile_id: string;

@Column({ nullable: true })
dob: Date;

@Column({ type: 'longtext', nullable: true })
bio: string;

@Column({ nullable: true })
avatar_url: string;

@Column({ nullable: true })
address: string;

@Column()
phone_number: string;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */

@OneToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
}

In above example, the Profile entity has a user property annotated with @OneToOne() and @JoinColumn(). This setup indicates that the Profile entity holds the foreign key reference to the User entity.

  • Bidirectional relationships:
// profile.entity.ts
import { Entity, Column, PrimaryColumn, OneToOne, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, JoinColumn } from 'typeorm';
import { User } from './user.entity';

@Entity()
export class Profile {
@PrimaryColumn({ generated: "uuid" })
profile_id: string;

@Column({ nullable: true })
dob: Date;

@Column({ type: 'longtext', nullable: true })
bio: string;

@Column({ nullable: true })
avatar_url: string;

@Column({ nullable: true })
address: string;

@Column()
phone_number: string;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */

@OneToOne(() => User, (user) => user.profile, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user: User;
}

// user.entity.ts
import { Entity, Column, PrimaryColumn, OneToOne, DeleteDateColumn, UpdateDateColumn, CreateDateColumn } from 'typeorm';
import { Profile } from './profile.entity';

@Entity()
export class User {
@PrimaryColumn({ generated: "uuid" })
user_id: string;

@Column()
name: string;

@Column()
email: string;

/*
* Select indicates row selection in QueryBuilder
* Default value is "true".
*/
@Column({ select: false })
password: string;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */

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

In this bidirectional setup, the User entity also has a profile property annotated with @OneToOne(). This ensures that both entities are aware of the relationship.

User Entity and Order Entity (One-to-Many | Many-to-One)

  • Unidirectional relationships:
// user.entity.ts
import { Entity, Column, PrimaryColumn } from 'typeorm';

@Entity()
export class User {
@PrimaryColumn({ generated: "uuid" })
user_id: string;

@Column()
name: string;

@Column()
email: string;

/*
* Select indicates row selection in QueryBuilder
* Default value is "true".
*/
@Column({ select: false })
password: string;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */
}


// order.entity.ts
import { Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm';

@Entity()
export class Order {
@PrimaryColumn({ generated: "uuid" })
order_id: string;

@Column()
total: number;

@Column()
ordered_on: Date

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
}
  • Bidirectional relationships:
// user.entity.ts
import { Entity, Column, PrimaryColumn, DeleteDateColumn, UpdateDateColumn, CreateDateColumn, OneToMany } from 'typeorm';
import { Order } from 'src/order/entities/order.entity';

@Entity()
export class User {
@PrimaryColumn({ generated: "uuid" })
user_id: string;

@Column()
name: string;

@Column()
email: string;

/*
* Select indicates row selection in QueryBuilder
* Default value is "true".
*/
@Column({ select: false })
password: string;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */

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


// order.entity.ts
import { User } from 'src/user/entities/user.entity';
import { Entity, Column, PrimaryColumn, ManyToOne, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, JoinColumn } from 'typeorm';

@Entity()
export class Order {
@PrimaryColumn({ generated: "uuid" })
order_id: string;

@Column()
total: number;

@Column()
ordered_on: Date

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */
@ManyToOne(() => User, user => user.orders)
@JoinColumn({ name: 'user_id' })
user: User;
}

@OneToMany is always an inverse side of the relation, and it can’t exist without @ManyToOneon the other side of the relation. In many-to-one / one-to-many relations, the owner side is always many-to-one. At owner side @JoinColumn is must. It means that the class that uses @ManyToOne will store the ID of the related object.

Order Entity and Product Entity (Many-to-Many)

  • Unidirectional relationships:
// product.entity.ts
import { Entity, Column, PrimaryColumn } from 'typeorm';

@Entity()
export class Product {
@PrimaryColumn({ generated: "uuid" })
product_id: string;

@Column()
name: string;

@Column({ type: 'longtext' })
description: string

@Column()
price: number;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */
}


// order.entity.ts
import { Entity, Column, PrimaryColumn, ManyToMany, JoinTable } from 'typeorm';

@Entity()
export class Order {
@PrimaryColumn({ generated: "uuid" })
order_id: string;

@Column()
total: number;

@Column()
ordered_on: Date

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */
@ManyToMany(() => Product)
@JoinTable()
products: Product[];
}

In this unidirectional setup, the Order entity has a products property annotated with @ManyToMany() and @JoinTable(). This setup indicates that the Order entity holds the foreign key reference to the Product entity.

After you run the application with the above code, the ORM will create an order_products_product junction table, which holds product_id as the foreign key to order.

  • Bidirectional relationships:
// order.entity.ts
import { Product } from 'src/product/entities/product.entity';
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, JoinColumn, ManyToMany, JoinTable } from 'typeorm';

@Entity()
export class Order {
@PrimaryColumn({ generated: "uuid" })
order_id: string;

@Column()
total: number;

@Column()
ordered_on: Date

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */

@ManyToMany(() => Product, product => product.orders)
@JoinTable({ name: "order_products" })
products: Product[];
}


// product.entity.ts
import { Order } from "src/order/entities/order.entity";
import { Column, CreateDateColumn, DeleteDateColumn, Entity, ManyToMany, PrimaryColumn, UpdateDateColumn } from "typeorm";

@Entity()
export class Product {
@PrimaryColumn({ generated: "uuid" })
product_id: string;

@Column()
name: string;

@Column({ type: 'longtext' })
description: string

@Column()
price: number;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */

@ManyToMany(() => Order, order => order.products)
orders: Order[];
}

In this bidirectional setup, the Product entity also has an orders property annotated with @ManyToMany(). This ensures that both entities are aware of the relationship and can reference each other.

@JoinTable is required to specify that this is the owner side of the relationship. After you run the application with the above code, the ORM will create an order_products junction table (because we defined the name of the table while using @JoinTable), which holds both primary key mappings.

Product Entity and Category Entity (One-to-Many | Many-to-One)

  • Unidirectional relationships:
// category.entity.ts
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';

@Entity()
export class Category {
@PrimaryColumn({ generated: "uuid" })
category_id: string;

@Column()
name: string;

@Column({ type: 'text', nullable: true })
description: string;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */
}


// product.entity.ts
import { Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm';

@Entity()
export class Product {
@PrimaryColumn({ generated: "uuid" })
product_id: string;

@Column()
name: string;

@Column({ type: 'longtext' })
description: string

@Column()
price: number;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */

@ManyToOne(() => Category)
category: Category;
}
  • Bidirectional relationships:
// category.entity.ts
import { Product } from 'src/product/entities/product.entity';
import { Entity, Column, PrimaryColumn, OneToMany, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';

@Entity()
export class Category {
@PrimaryColumn({ generated: "uuid" })
category_id: string;

@Column()
name: string;

@Column({ type: 'text', nullable: true })
description: string;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */

@OneToMany(() => Product, product => product.category)
products: Product[];
}


// product.entity.ts
import { Category } from "src/category/entities/category.entity";
import { Column, CreateDateColumn, DeleteDateColumn, Entity, ManyToOne, PrimaryColumn, UpdateDateColumn } from "typeorm";

@Entity()
export class Product {
@PrimaryColumn({ generated: "uuid" })
product_id: string;

@Column()
name: string;

@Column({ type: 'longtext' })
description: string

@Column()
price: number;

@CreateDateColumn()
created_on: Date

@UpdateDateColumn()
updated_on: Date

@DeleteDateColumn({ nullable: true })
deleted_on: Date;

/* previous relationship if any */

@ManyToOne(() => Category, category => category.products)
@JoinColumn({ name: 'category_id' })
category: Category;
}

I have used bidirectional relationships for all entities. This approach provides more flexibility in select conditions but increases execution time.

We assume that a user can order multiple products in a single quantity. For the above setup, we can’t store the quantity of products ordered by a user. To address this, we need to store additional information, such as the quantity, either in a separate entity or by updating the relationship between Order and Product to many-to-one and adding a quantity column.

Note: I have all entities with bidirectional relationships. Add additional columns to entities as necessary

Class Variables and Their Treatment

In the context of entities:

  • Class Variables: These are properties defined within a class. They represent the attributes of the entity.
  • Columns: When you use TypeORM decorators like @Column, these class variables are treated as columns in the database table.
  • Data Types: The TypeScript type of each class variable determines the SQL data type of the corresponding column. For example, a TypeScript string is mapped to a SQL VARCHAR or TEXT type.

Setting Up Entities in TypeOrm Module

Now we need to set the entities to be managed by TypeORM. For that, we need to set entities in the TypeOrmModule in the imports of the module, as shown below:

// user.module.ts
/* previous imports */
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Profile } from './entities/profile.entity';

@Module({
imports: [TypeOrmModule.forFeature([User, Profile])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule { }


//product.module.ts
/* previous imports */
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from './entities/product.entity';

@Module({
imports:[TypeOrmModule.forFeature([Product])],
controllers: [ProductController],
providers: [ProductService],
})
export class ProductModule {}


//order.module.ts
/* previous imports */
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from './entities/order.entity';

@Module({
imports: [TypeOrmModule.forFeature([Order])],
controllers: [OrderController],
providers: [OrderService],
})
export class OrderModule { }


//category.module.ts
/* previous imports */
import { TypeOrmModule } from '@nestjs/typeorm';
import { Category } from './entities/category.entity';

@Module({
imports: [TypeOrmModule.forFeature([Category])],
controllers: [CategoryController],
providers: [CategoryService],
})
export class CategoryModule { }

Execution and Output

Before execution of application. If you are following NestJS series of my articles, we have to make some changes in previous implemented code i.e in product.service.ts file.

/* imports */
@Injectable()
export class ProductService {
constructor(
@InjectRepository(Product)
private readonly productRepo: Repository<Product>,
) { }
/* other methods */

async findOne(id: string): Promise<Product> {
return this.productRepo.findOneBy({ product_id: id }) // changed id to product_id
}

async remove(id: string): Promise<string> {
this.productRepo.delete({ product_id: id }); // changed id to product_id
return "Product deleted successfully";
}
}

Make sure that database is present and TypeOrm synchronize to true. Now start the application using below command in terminal.

$ nest start

For the first time it will take some time, because the TypeOrm should sync entities to database. The database may look like given below image:

Note: After execution make the TypeOrm synchronize to false and restart the application, this can avoid conflicts in database.

Tips and Best Practices

  • Always use the appropriate relationship decorators (@OneToOne, @OneToMany, @ManyToOne, @ManyToMany) to define relationships between entities.
  • Utilize @JoinColumn and @JoinTable where necessary to specify the owning side of the relationship.
  • Keep your entity classes clean and focused on data structure and relationships.
  • Ensure that your database schema aligns with your entity definitions.
  • For code robustness and better readability, consider using a base entity class where all columns common to entities exist, and have other entities extend this base entity.

For more details about TypeORM, please refer to the TypeORM Documentation and TypeORM gitbook documentation.

Conclusion

Understanding and implementing entities and their relationships is crucial for building robust and scalable applications with NestJS. By leveraging TypeORM’s powerful features, you can easily manage complex relationships between your data models. With the examples and best practices provided in this article, you are now equipped to define and work with entities and their relationships in your NestJS applications.

In next article's we will look into CRUD operations.

GitHub Repository:
You can find the complete code for this implementation on GitHub: bhargavachary123

Contribution

If you like my Article, please, consider donating through Buy Me a Coffee: https://buymeacoffee.com/bhargavachary.

If you found this article useful, please consider leaving a clap (👏) and a comment.

Stay tuned for more articles covering advanced topics in NestJS development! Happy coding ⚔️

--

--

Bhargava Chary
Bhargava Chary

No responses yet