(NestJS-6) CRUD Operations in NestJS Using TypeORM: An E-commerce Example

Bhargava Chary
14 min readSep 5, 2024

--

Introduction

In this article, we continue our journey through building a robust e-commerce application using NestJS and TypeORM. Having covered the basics, custom configurations, database connections, and entity relationships in previous articles, we now focus on implementing CRUD (Create, Read, Update, Delete) operations. This guide will use the entities defined in our previous discussions: User, Profile, Order, Product, and Category.

Ensure you have followed the previous articles in this series to have the necessary setup, If not refer here.

Setting Up the Project

Before diving into the CRUD operations, ensure your project is set up correctly with the necessary modules and configurations. If you’re starting fresh, make sure you have:

  1. A NestJS project setup with TypeORM.
  2. Entities defined (User, Profile, Order, Product, Category) with their respective relationships.
  3. Modules generated using the NestJS resource generator.

Summary of HTTP Methods which are used in CRUD Operations

Standard and Mostly used status codes in HTTP requests to indicate result

These status codes can be easily accessed using the HttpStatus package in @nestjs/common.

The below image has all entities with relations up to date till CRUD development. I have added some validations to entity variables. Please make sure to update it by referring to the image below.

TypeORM repository methods.

1. create()

  • Purpose: Creates a new instance of an entity. This method doesn’t persist anything to the database. It only returns an instance of the entity populated with the provided data.
  • Usage: This method is useful when you need to construct an entity object before inserting or saving it into the database.

2. insert()

  • Purpose: Inserts one or multiple entities into the database. This method performs a low-level SQL INSERT and bypasses lifecycle hooks or related entities management.
  • Note: It does not load entities into memory and doesn’t manage entity relationships (e.g., it won’t update join tables in many-to-many relations). If relations need to be inserted, use save() .
  • Usage: Use this for bulk or fast inserts without needing to load entities.

3. update()

  • Purpose: Updates existing records in the database. This method generates a direct SQL UPDATE query and doesn't load the entity into memory or manage relations.
  • Note: Similar to insert(), update() won't handle cascading relations. If relations need to be updated, use save().

4. find()

  • Purpose: Retrieves all records from the table. You can apply conditions, relations, pagination, and ordering through options.
  • Usage: This method is typically used to get a list of entities.

5. findOne()

  • Purpose: Retrieves one entity that matches the provided conditions. You can also request to load its relations.
  • Usage: Use this when you need a single record, often by specifying primary key or unique identifiers.

6. findOneBy()

  • Purpose: Retrieves one entity by a given set of criteria, without the ability to include relations. It’s useful when relations aren’t necessary.
  • Usage: Commonly used for simpler queries when you just want to retrieve a single entity by matching a few fields.

7. delete()

  • Purpose: Deletes one or more records from the database. This performs a permanent removal of the data.
  • Usage: Ideal when you need to fully remove a record from the database.

8. softDelete()

  • Purpose: Marks the record as deleted without actually removing it from the database. Instead, it sets a flag (e.g., deletedAt timestamp). The data can still be retrieved using special queries, and it can even be restored if needed.
  • Usage: Useful for implementing “soft delete” logic, where records are not permanently deleted but are hidden from normal queries.

9. restore()

  • Purpose: Reverts a soft-deleted entity back to its original state, making it available again in regular queries.
  • Usage: Use this method to “undelete” or restore a soft-deleted record.

10. save()

  • Purpose: Inserts or updates an entity based on whether the primary key exists. It handles cascading relations and triggers lifecycle events (e.g., beforeInsert, beforeUpdate).
  • Usage: Use this when you want TypeORM to manage the insertion or update of an entity and its related entities.

11. count()

  • Purpose: Counts the number of entities that match the given conditions. It’s useful for determining how many records match a certain query.

12. findAndCount()

  • Purpose: Performs a find() query and also returns the count of matching entities in one call.
  • Usage: Useful when you need both the data and the total count for pagination.

13. query()

  • Purpose: Executes raw SQL queries. This method bypasses TypeORM’s abstraction layer and allows executing any SQL statement directly.
  • Usage: Useful for complex queries or when you need to perform raw SQL operations.

For our CRUD operations, we will use the following methods: insert(), update(), save(), find(), findOne(), findOneBy(), and softDelete().

Setting Up CRUD Operations.

In this section, we’ll demonstrate CRUD operations using the User entity.

1. Create a User.

Define DTO to create a new User.

Create User DTO (dto/create-user.dto.ts):


export class CreateUserDto {
name: string;
email: string;
password: string;
address: string;
phoneno: string
dob?: Date;
bio?: string;
}

User Service (user.service.ts):

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { Profile } from './entities/profile.entity';
import { User } from './entities/user.entity';

@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(Profile)
private profileRepository: Repository<Profile>,
) {}

async create(createUserDto: CreateUserDto) {
// Create a new User instance
const user = new User();
user.name = createUserDto.name;
user.password = createUserDto.password;
user.email = createUserDto.email;
/*
* Save the new user to the repository.
* We used the `save` method instead of `insert`. The `save` method will return the User object after insertion or update, unlike `insert`.
*/
const new_user = await this.userRepository.save(user);

// Create a new Profile instance for the user
const profile = new Profile();
profile.address = createUserDto.address;
profile.phone_number = createUserDto.phoneno;
profile.bio = createUserDto.bio;
profile.dob = createUserDto.dob;
profile.user = new_user; // we can use {user_id:new_user.user_id } as User;
await this.profileRepository.insert(profile)
// Save the profile and assign it to the user
return { message: "User Created Successfully", user_Id: new_user.user_id };
}
}

In the above code, we’re creating a User and their corresponding Profile. The Profile linked to the User through the user relationship.

To clarify, the insert() method in TypeORM does not automatically create or manage relations between entities. The insert() method performs a low-level SQL INSERT operation, bypassing the entity manager and not handling relationships or cascading. In such cases, you should use the save() method, which properly manages both the main entity and its relations. The same solution applies to the update() method as well.

User Controller (user.controller.ts):

import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@Post()
async create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
}

2. Read All Existing Users

To fetch all users, we’ll create a method in the service and expose it through the controller.

User Controller (user.controller.ts):

@Get()
async findAll() {
return this.userService.findAll();
}

User Service (user.service.ts):

async findAll(): Promise<User[]> {
return await this.userRepository.find({ relations: { profile: true } });
}

3. Read a Single User

To fetch a single user by ID, we’ll define the method in both the service and the controller.

User Controller (user.controller.ts):

@Get(':id')
async findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}

User Service (user.service.ts):

async findOne(id: string): Promise<User> {
return await this.userRepository.findOne({ where: { user_id: id }, relations: { profile: true } });
}

4. Update a User

To update a user, we’ll create an update DTO and a service method to handle the update logic.

Update User DTO (dto/update-user.dto.ts):

export class UpdateUserDto {
id: string;
name: string;
email: string;
address: string;
dob?: Date;
bio?: string;
phoneno?:string;
}

User Service (user.service.ts):

 async update(updateUserDto: UpdateUserDto): Promise<string> {
const user = await this.findOne(updateUserDto.id);
if (!user)
return "User not found";

await this.userRepository.update({ user_id: updateUserDto.id }, {
name: updateUserDto.name,
email: updateUserDto.email,
})
await this.profileRepository.update({ user: { user_id: user.user_id } }, {
address: updateUserDto.address,
dob: updateUserDto.dob,
bio: updateUserDto.bio,
phone_number: updateUserDto.phoneno
})
return "User Updated Successfully";
}

In the above code, we’re updateing a User and their corresponding Profile.

User Controller (user.controller.ts):

  @Put()
async update(@Body() updateUserDto: UpdateUserDto) {
return this.userService.update(updateUserDto);
}

5. Delete a User

To delete a user, we’ll create a method in the service and expose it through the controller.
A record can be deleted in 2 ways:

Direct Delete: This method removes the record permanently from the database. It is irreversible, meaning the data is lost once deleted.

Soft Delete: This method marks the record as deleted without actually removing it from the database. This is useful for retaining historical data or allowing for recovery of deleted records.

In our User entity, soft delete is implemented using the @DeleteDateColumn() decorator. When a record is soft deleted, the deleted_on column is populated with a timestamp, and the record is excluded from future queries by default.

User Controller (user.controller.ts):

  @Delete(':id')
async remove(@Param('id') id: string) {
return this.userService.remove(id);
}

User Service (user.service.ts):

  async remove(id: string) {
const user = await this.userRepository.findOne({ where: { user_id: id }, relations: { profile: true } });
if (!user)
return "User not exists!"
await this.profileRepository.softDelete({ profile_id: user.profile.profile_id })
await this.userRepository.softDelete({ user_id: id })
return "User Deleted Successfully";
}

Now we are deleting both the user and profile.

In this example, when a user is deleted, their profile is also soft deleted. If you try to delete the same user again, the application will return a “User not exists!” message, although the record still exists in the database but is marked as deleted.

If the deleted_on column is filled with data (indicating that the record is soft deleted), TypeOrm will not fetch it by default when querying for records. To retrieve such soft-deleted records or include logic for handling them, you'll need to use specific methods or configurations OR you can verify created records in database.

From Here we will try to create all the service in all modules with there relation.

Category Module

category create and update DTO:

// dto/create-category.dto
export class CreateCategoryDto {
name: string;
description?: string;
}

// dto/update-category.dto
export class UpdateCategoryDto {
id: string;
name: string;
description?: string;
}

category.service.ts:

// category/category.service.ts

import { Injectable } from '@nestjs/common';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Category } from './entities/category.entity';
import { Repository } from 'typeorm';

@Injectable()
export class CategoryService {
constructor(
@InjectRepository(Category)
private readonly categoryRepo: Repository<Category>,
) { }

async create(createCategoryDto: CreateCategoryDto) {
const new_category = new Category();
new_category.name = createCategoryDto.name;
new_category.description = createCategoryDto.description;
const category = await this.categoryRepo.save(new_category)
return { message: "Category created succesfully", category_Id: category.category_id };
}

async findAll(): Promise<Category[]> {
return await this.categoryRepo.find();
}

async findOne(id: string): Promise<Category> {
return await this.categoryRepo.findOneBy({ category_id: id });
}

async update(updateCategoryDto: UpdateCategoryDto): Promise<string> {
const category = await this.categoryRepo.findOneBy({ category_id: updateCategoryDto.id });
if (!category) {
return 'Category not found';
}
await this.categoryRepo.update(updateCategoryDto.id, updateCategoryDto)
return 'Category updated succesfully';
}

async remove(id: string): Promise<string> {
const category = await this.categoryRepo.findOneBy({ category_id: id });
if (!category) {
return 'Category not found';
}
await this.categoryRepo.softDelete({ category_id: id });
return 'Category deleted succesfully';
}
}

category.controller.ts:

// category/category.controller.ts

import { Controller, Get, Post, Body, Param, Delete, Put } from '@nestjs/common';
import { CategoryService } from './category.service';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';

@Controller('category')
export class CategoryController {
constructor(private readonly categoryService: CategoryService) { }

@Post()
async create(@Body() createCategoryDto: CreateCategoryDto) {
return this.categoryService.create(createCategoryDto);
}

@Get()
async findAll() {
return this.categoryService.findAll();
}

@Get(':id')
async findOne(@Param('id') id: string) {
return this.categoryService.findOne(id);
}

@Put()
async update(@Body() updateCategoryDto: UpdateCategoryDto) {
return this.categoryService.update(updateCategoryDto);
}

@Delete(':id')
async remove(@Param('id') id: string) {
return this.categoryService.remove(id);
}
}

Products

product create and update DTO:

// dto/create-product.dto
export class CreateProductDto {
name: string;
description: string;
price: number;
categoryId: string;
}

// dto/update-product.dto
export class UpdateProductDto {
id: string;
name: string;
description: string;
price: number;
}

product.module.ts

We will try to access category entity in products. importing category entity in imports of module to use the repository in controller and service files of product module.

import { Module } from '@nestjs/common';
import { ProductService } from './product.service';
import { ProductController } from './product.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from './entities/product.entity';
import { CategoryService } from 'src/category/category.service';
import { Category } from 'src/category/entities/category.entity';

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

product.service.ts:

// src\product\product.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Product } from './entities/product.entity';
import { Repository } from 'typeorm';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
import { CategoryService } from 'src/category/category.service';

@Injectable()
export class ProductService {
constructor(
@InjectRepository(Product)
private readonly productRepo: Repository<Product>,
private readonly categoryService: CategoryService,
) { }
async create(createProductDto: CreateProductDto) {
const category = await this.categoryService.findOne(createProductDto.categoryId);
if (!category) {
return 'Category not found';
}
const newProduct = new Product(); // Creating a product object to initialize its values
newProduct.name = createProductDto.name;
newProduct.description = createProductDto.description;
newProduct.price = createProductDto.price;
newProduct.category = category; // Directly assign the found category
await this.productRepo.insert(newProduct); // insert the new product in the product table
return "Product created successfully";
}

async findAll(): Promise<Product[]> {
return await this.productRepo.find(); //returning all the products
}

async findOne(id: string): Promise<Product> {
return await this.productRepo.findOneBy({ product_id: id }) //returning a specific product
}

async update(updateProductDto: UpdateProductDto) {
const product = await this.findOne(updateProductDto.id);
if (!product)
return "Product no"
await this.productRepo.update(updateProductDto.id, updateProductDto); // updating the product
return "Product updated sucessfullly";
}

async remove(id: string): Promise<string> {
await this.productRepo.softDelete({ product_id: id }); // Mark as Deleted the product if exists, Does not check if entity exist in the database.
/*
the soft delete will not delete the object in table but it will make that object has been deleted by adding the date in deleted_on column. here we can retrive the value if needed.
OR
the delete will delete entire object from table and can't be retrived back
*/
return "Product deleted successfully";
}
}

product.controller.ts:

// src\product\product.controller.ts

import { Controller, Get, Post, Body, Param, Delete, Put } from '@nestjs/common';
import { ProductService } from './product.service';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';

@Controller('product')
export class ProductController {
constructor(private readonly productService: ProductService) { }

@Post()
async create(@Body() createProductDto: CreateProductDto) {
return this.productService.create(createProductDto);
}

@Get()
async findAll() {
return this.productService.findAll();
}

@Get(':id')
async findOne(@Param('id') id: string) {
return this.productService.findOne(id);
}
@Put()
async update(@Body() updateProductDto: UpdateProductDto) {
return this.productService.update(updateProductDto);
}

@Delete(':id')
async remove(@Param('id') id: string) {
return this.productService.remove(id);
}
}

Order

order create and update DTO:

// dto/create-order.dto
export class CreateOrderDto {
userId: string;
productIds: string[];
}

// dto/update-order.dto
export class UpdateOrderDto {
orderId: string;
productIds: string[]; // updated products list
}

order.module.ts

To access the ProductService and UserService in the OrderModule, we need to include these services in the providers array. Additionally, we import the repositories and services for the related entities due to their interdependencies and relationships. This ensures that the OrderService has access to the necessary functionalities for handling orders, users, products, and categories.

import { Module } from '@nestjs/common';
import { OrderService } from './order.service';
import { OrderController } from './order.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from './entities/order.entity';
import { UserService } from 'src/user/user.service';
import { ProductService } from 'src/product/product.service';
import { User } from 'src/user/entities/user.entity';
import { Profile } from 'src/user/entities/profile.entity';
import { Product } from 'src/product/entities/product.entity';
import { CategoryService } from 'src/category/category.service';
import { Category } from 'src/category/entities/category.entity';

@Module({
imports: [TypeOrmModule.forFeature([Order, User, Profile, Product, Category])],
controllers: [OrderController],
providers: [OrderService, UserService, ProductService, CategoryService],
})
export class OrderModule { }

order.service.ts:

// src\order\order.service.ts

import { Injectable } from '@nestjs/common';
import { CreateOrderDto } from './dto/create-order.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Order } from './entities/order.entity';
import { Repository } from 'typeorm';
import { UserService } from 'src/user/user.service';
import { ProductService } from 'src/product/product.service';
import { UpdateOrderDto } from './dto/update-order.dto';
import { Product } from 'src/product/entities/product.entity';

@Injectable()
export class OrderService {
constructor(
@InjectRepository(Order)
private readonly orderRepo: Repository<Order>,
private readonly userService: UserService,
private readonly productService: ProductService,
) { }

async create(createOrderDto: CreateOrderDto): Promise<string> {
// Fetch the user to associat with the order
const user = await this.userService.findOne(createOrderDto.userId);
if (!user) {
return 'User not found';
}

const order = new Order(); // Create a new order instance
order.user = user;
let total: number = 0;
// Fetch and process each product
const products = await Promise.all(createOrderDto.productIds.map(async (productId) => {
const product = await this.productService.findOne(productId);
if (product) {
// Accumulate total price
total += parseInt(product.price + "");
return product;
}
}));
order.products = products; // set products to order
order.total = total;
order.ordered_on = new Date();
await this.orderRepo.save(order); // Save the new order to the repository
return "Ordered successfully";
}

async update(updateOrderDto: UpdateOrderDto): Promise<string> {
const order = await this.orderRepo.findOne({ where: { order_id: updateOrderDto.orderId }, relations: { products: true } })
let total: number = 0;
const new_products = await Promise.all(updateOrderDto.productIds.map(async (productId) => {
const product = await this.productService.findOne(productId);
if (product) {
// Accumulate total price
total += parseInt(product.price + "");
return product;
}
}));
if (!order)
return "Order not exists!"
order.products = new_products;
order.total = total
await this.orderRepo.save(order)
return "Order updated successfully";
}

async findAll(): Promise<Order[]> {
return await this.orderRepo.find({ relations: { products: true }, select: { products: { product_id: true } } });
}

async findOne(id: string): Promise<Order> {
return await this.orderRepo.findOne({
where: { order_id: id },
select: { user: { user_id: true, name: true, profile: { profile_id: true, address: true } } }
});
}

async remove(id: string): Promise<string> {
await this.orderRepo.softDelete(id)
return "Order deleted succesfully";
}
}

order.controller.ts:

// src\order\order.controller.ts

import { Controller, Get, Param, Delete, Post, Body, Put } from '@nestjs/common';
import { OrderService } from './order.service';
import { CreateOrderDto } from './dto/create-order.dto';
import { UpdateOrderDto } from './dto/update-order.dto';

@Controller('order')
export class OrderController {
constructor(private readonly orderService: OrderService) { }
@Post()
async create(@Body() createOrderDto: CreateOrderDto) {
return this.orderService.create(createOrderDto);
}

@Put()
async update(@Body() updateOrderDto: UpdateOrderDto) {
return this.orderService.update(updateOrderDto);
}

@Get()
async findAll() {
return this.orderService.findAll();
}

@Get(':id')
async findOne(@Param('id') id: string) {
return this.orderService.findOne(id);
}

@Delete(':id')
async remove(@Param('id') id: string) {
return this.orderService.remove(id);
}
}

Execution and Output

Now start the application using below command in terminal.

$ nest start

Once the application is up. Try the all API’s in postman.

Category, Product & Order CRUD test images using Postman:

Create category and products
Get existing category's and products
create and update order
Before and After update order

To fetch a orders of a user, we will implemented below service in user module, which will fetch all the orders of user which are registered by user.

 // user.controller.ts:
@Get('userorders/:id')
async findOneUserOrders(@Param('id') id: string) {
return this.userService.findOneUserOrders(id);
}


// user.service.ts:
async findOneUserOrders(id: string): Promise<User> {
return await this.userRepository.findOne({
where: { user_id: id },
relations: { orders: { products: true } },
select: {
user_id: true, name: true,
orders: {
order_id: true, ordered_on: true, total: true,
products: { product_id: true, name: true, price: true }
}
}
});
}
user orders along with products

Conclusion

In this article, we’ve implemented CRUD operations for our e-commerce application using NestJS and TypeORM. We’ve covered how to handle entities and their relationships, CRUD operation's.

In next article's we will look into different validators to ensure data consistency.

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