How to build an ecommerce app with NestJS
Tags: node, nestjs, webdev
Published: 2022-06-23 15:52:00

**Written by [Ivaylo Gerchev](https://blog.logrocket.com/author/ivaylogerchev/)✏️**

NestJS is one of the best Node frameworks for building server-side applications. In this tutorial, we'll explore how to build a simple NestJS ecommerce app, demonstrating many of Nest’s major features along the way. We’ll cover:

*   [Getting started with our NestJS ecommerce app](#getting-started-nestjs-ecommerce-app)
*   [Creating the NestJS ecommerce store product feature](#creating-nestjs-ecommerce-store-product-feature)
*   [Creating the user management feature](#creating-user-management-feature)
*   [Creating user authentication and authorization](#creating-user-authentication-authorization)
*   [Creating the store cart feature for our NestJS ecommerce app](#creating-store-cart-feature-nestjs-ecommerce-app)

## Getting started with our NestJS ecommerce app <a name="getting-started-nestjs-ecommerce-app">

By default, [NestJS](https://nestjs.com/) uses [Express](http://expressjs.com/) under the hood, although you have the option to use [Fastify](https://www.fastify.io/) instead. Nest provides a solid application architecture, while Express and Fastify are strong HTTP server frameworks with a myriad of features for app development. 

Having robust architecture gives you the ability to build highly scalable, testable, loosely coupled, and easy-to-maintain applications. Using Nest enables you to [take your Node.js backend to the next level](https://blog.logrocket.com/node-back-end-next-level-nestjs/). 

Nest is heavily inspired by [Angular](https://angular.io/) and borrows many of its concepts. If you already use Angular, Nest could be the perfect match. 

To follow this tutorial, you will need at least basic knowledge of and experience with Node, MongoDB, TypeScript, and Nest. Make sure you have [Node](https://nodejs.org/en/) and [MongoDB](https://www.mongodb.com/) installed on your machine.

### Nest features you should know

Let’s take a moment to review the main Nest features: modules, controllers, and services. 

Modules are the main strategy to organize and structure Nest app. There must be at least one root module to create an app. Each module can contain controllers and services, and even other modules. 

Nest uses the dependency injection pattern to join modules with their dependencies. To make a class injectable, Nest uses an `@Injectable` decorator. Then, to provide the class in a module or in a controller, it uses the constructor-based dependency injection. 

Controllers handle incoming HTTP requests, validate parameters, and return responses to the client. Controllers should be kept clean and simple, which is where the next Nest feature comes into play. 

Services hold most of the business logic and app functionality for your Nest projects. Any complex logic should be provided via services. In fact, services fall under a main type of class called providers. 

A provider is just a class injected as a dependency. Other types of a provider which might be used include classes like repositories, factories, helpers, etc.

### Creating a new Nest project for our ecommerce app

When you're ready, let's initialize a new Nest project. First, we’ll install Nest CLI. Then, we will create a new project:

```shell
npm install -g @nestjs/cli
nest new nestjs-ecommerce
```

After installation is complete, navigate to the project and start it:

```shell
cd nestjs-ecommerce
npm run start:dev
```

You can then launch the app in your browser by visiting http://localhost:3000/. You should see a nice “Hello World!” message. 

The app will reload automatically after any changes you make. If you want to restart the app manually, use `npm run start` command instead. 

Now we’re ready to start creating the store features.

## Creating the NestJS ecommerce store product feature <a name="creating-nestjs-ecommerce-store-product-feature">

In this section, we'll focus on product management. The store product feature will allow us to retrieve store products, add new ones, and edit or delete them.

### Creating our product resources

Let’s start by creating the needed resources. To create them, run the following commands:

```shell
nest g module product
nest g service product --no-spec
nest g controller product --no-spec 
```

The first command generates a product module and puts it in its own directory with the same name. 

The next two commands generate service and controller files and import them automatically in the `product` module. The `--no-spec` argument tells Nest that we don't want to generate additional test files. 

After running the above commands, we’ll get a new `product` directory containing the following files: `product.module.ts`, `product.service.ts`, and `product.controller.ts`. 

Now we have a basic structure for the NestJS ecommerce store product feature. Before we move on, we need to set up our database.

### Configuring the MongoDB database

As we are using MongoDB as a database, we'll need to install `mongoose` and `@nestjs/mongoose` packages.

```shell 
npm install --save @nestjs/mongoose mongoose
```

After the installation is complete, open `app.module.ts` and replace its content with the following:

```typescript 
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; // 1.1 Import the mongoose module
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductModule } from './product/product.module'; // 2.1 Import the product module

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/store'), // 1.2 Setup the database
    ProductModule, // 2.2 Add the product module
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

Here’s what we did in the code above. Follow along using my numbered notes:

*   First, we imported the `MongooseModule` (1.1) and used it to set up a new `store` database (1.2)
*   Second, we imported the `ProductModule` (2.1) and added it to the `imports` array (2.2)

Our next step is to create a database schema for our product model.

### Creating a product model schema

In the `product` directory, create a new `schemas` directory. Put a `product.schema.ts` file in the new directory with the following content:

```typescript 
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type ProductDocument = Product & Document;

@Schema()
export class Product {
  @Prop()
  name: string;

  @Prop()
  description: string;

  @Prop()
  price: number;

  @Prop()
  category: string;
}

export const ProductSchema = SchemaFactory.createForClass(Product);
```

The code above creates a schema for our product with `name`, `description`, `price`, and `category` properties. 

Now edit the `product.module.ts` in the following manner:

```typescript 
import { Module } from '@nestjs/common';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';
import { MongooseModule } from '@nestjs/mongoose'; // 1\. Import mongoose module
import { ProductSchema } from './schemas/product.schema'; // 2\. Import product schema

@Module({
  imports: [
    MongooseModule.forFeature([{ name: 'Product', schema: ProductSchema }]) // 3\. Setup the mongoose module to use the product schema
  ],
  controllers: [ProductController],
  providers: [ProductService]
})
export class ProductModule {}
```

As you can see from my numbered notes, in the code above, we imported the `MongooseModule` (1) and `ProductModule` (2), then set the `ProductSchema` to be used for our product model (3).

### Creating product DTO files

In addition to the product schema, we’ll also need two Data Transfer Object (DTO) files for our NestJS ecommerce app. A DTO file defines the data which will be received from a form submission, a search query, and so on. 

We need one DTO for product creation and another for product filtering. Let’s create them now. 

In the `product` directory, create a new `dtos` directory. Put a `create-product.dto.ts` file in this new directory with the following content:

```typescript 
export class CreateProductDTO {
  name: string;
  description: string;
  price: number;
  category: string;
}
```

The above DTO defines a product object with the necessary properties for new product creation. 

Then, in the same directory, create a `filter-product.dto.ts` file with the following content:

```typescript
export class FilterProductDTO {
  search: string;
  category: string;
}
```

This second DTO defines a filter object, which we’ll use to filter the store products by search query, category, or both.

### Creating product service methods

All the prep work for this section is done. Now let’s create the actual code for product management. 

Open the `product.service.ts` file and replace its content with the following:

```typescript 
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Product, ProductDocument } from './schemas/product.schema';
import { CreateProductDTO } from './dtos/create-product.dto';
import { FilterProductDTO } from './dtos/filter-product.dto';

@Injectable()
export class ProductService {
  constructor(@InjectModel('Product') private readonly productModel: Model<ProductDocument>) { }

  async getFilteredProducts(filterProductDTO: FilterProductDTO): Promise<Product[]> {
    const { category, search } = filterProductDTO;
    let products = await this.getAllProducts();

    if (search) {
      products = products.filter(product => 
        product.name.includes(search) ||
        product.description.includes(search)
      );
    }

    if (category) {
      products = products.filter(product => product.category === category)
    }

    return products;
  }

  async getAllProducts(): Promise<Product[]> {
    const products = await this.productModel.find().exec();
    return products;
  }

  async getProduct(id: string): Promise<Product> {
    const product = await this.productModel.findById(id).exec();
    return product;
  }

  async addProduct(createProductDTO: CreateProductDTO): Promise<Product> {
    const newProduct = await this.productModel.create(createProductDTO);
    return newProduct.save();
  }

  async updateProduct(id: string, createProductDTO: CreateProductDTO): Promise<Product> {
    const updatedProduct = await this.productModel
      .findByIdAndUpdate(id, createProductDTO, { new: true });
    return updatedProduct;
  }

  async deleteProduct(id: string): Promise<any> {
    const deletedProduct = await this.productModel.findByIdAndRemove(id);
    return deletedProduct;
  }
}
```

Let’s examine the code block above piece by piece. 

First, let’s take a look at the section copied below:

```typescript 
@Injectable()
export class ProductService {
  constructor(@InjectModel('Product') private readonly productModel: Model<ProductDocument>) { }

}
```

This code injects the needed dependencies (the product model) by using the `@InjectModel` decorator. 

In the next section, we have two methods:

```typescript 
async getAllProducts(): Promise<Product[]> {
  const products = await this.productModel.find().exec();
  return products;
}

async getProduct(id: string): Promise<Product> {
  const product = await this.productModel.findById(id).exec();
  return product;
}
```

The first method `getAllProducts` is for getting all products. The second method `getProduct` is for getting a single product. We use standard Mongoose methods to achieve these actions. 

The method `getFilteredProducts` below returns filtered products:

```typescript 
async getFilteredProducts(filterProductDTO: FilterProductDTO): Promise<Product[]> {
  const { category, search } = filterProductDTO;
  let products = await this.getAllProducts();

  if (search) {
    products = products.filter(product => 
      product.name.includes(search) ||
      product.description.includes(search)
    );
  }

  if (category) {
    products = products.filter(product => product.category === category)
  }

  return products;
}
```

Products can be filtered by search query, by category, or by both. 

The next method `addProduct` below creates a new product:

```typescript 
async addProduct(createProductDTO: CreateProductDTO): Promise<Product> {
  const newProduct = await this.productModel.create(createProductDTO);
  return newProduct.save();
}
```

`addProduct` achieves this by using the class from the `create-product.dto.ts` file and saving it to the database. 

The final two methods are `updateProduct` and `deleteProduct`:

```typescript 
async updateProduct(id: string, createProductDTO: CreateProductDTO): Promise<Product> {
  const updatedProduct = await this.productModel
    .findByIdAndUpdate(id, createProductDTO, { new: true });
  return updatedProduct;
}

async deleteProduct(id: string): Promise<any> {
  const deletedProduct = await this.productModel.findByIdAndRemove(id);
  return deletedProduct;
}
```

Using these methods, you can find a product by ID and either update it or remove it from the database.

### Creating product controller methods

The final step for the product module is to create the API endpoints. 

We’ll create the following API endpoints:

*   POST `store/products/` — add new product
*   GET `store/products/` — get all products
*   GET `store/products/:id` — get single product
*   PUT `store/products/:id` — edit single product
*   DELETE `store/products/:id` — remove single product

Open the `product.controller.ts` file and replace its content with the following:

```typescript 
import { Controller, Post, Get, Put, Delete, Body, Param, Query, NotFoundException } from '@nestjs/common';
import { ProductService } from './product.service';
import { CreateProductDTO } from './dtos/create-product.dto';
import { FilterProductDTO } from './dtos/filter-product.dto';

@Controller('store/products')
export class ProductController {
  constructor(private productService: ProductService) { }

  @Get('/')
  async getProducts(@Query() filterProductDTO: FilterProductDTO) {
    if (Object.keys(filterProductDTO).length) {
      const filteredProducts = await this.productService.getFilteredProducts(filterProductDTO);
      return filteredProducts;
    } else {
      const allProducts = await this.productService.getAllProducts();
      return allProducts;
    }
  }

  @Get('/:id')
  async getProduct(@Param('id') id: string) {
    const product = await this.productService.getProduct(id);
    if (!product) throw new NotFoundException('Product does not exist!');
    return product;
  }

  @Post('/')
  async addProduct(@Body() createProductDTO: CreateProductDTO) {
    const product = await this.productService.addProduct(createProductDTO);
    return product;
  }

  @Put('/:id')
  async updateProduct(@Param('id') id: string, @Body() createProductDTO: CreateProductDTO) {
    const product = await this.productService.updateProduct(id, createProductDTO);
    if (!product) throw new NotFoundException('Product does not exist!');
    return product;
  }

  @Delete('/:id')
  async deleteProduct(@Param('id') id: string) {
    const product = await this.productService.deleteProduct(id);
    if (!product) throw new NotFoundException('Product does not exist');
    return product;
  }
}
```

NestJS provides [a full set of JavaScript decorators](https://blog.logrocket.com/understanding-javascript-decorators/) to work with HTTP requests and responses (`Get`, `Put`, `Body`, `Param`, etc.), handle errors (`NotFoundException`), define controllers (`Controller`), and so on. 

We imported the ones we need from `@nestjs/common` at the beginning of the file. We also import all the other files we’ve already created and we need: `ProductService`, `CreateProductDTO`, and `FilterProductDTO`. 

From now on, I won’t explain imports in great detail. Most of them are pretty straightforward and self-explanatory. For more information about a particular class or component’s use, you can [consult the documentation](https://www.typescriptlang.org/docs/handbook/decorators.html). 

Let’s divide the rest of the code into smaller chunks. 

First, we use `@Controller` decorator to set the the part of the URL which is shared by all endpoints:

```typescript 
@Controller('store/products')
export class ProductController {
  constructor(private productService: ProductService) { }
}
```

We also inject the product service in the class constructor in the code above. 

Next, we define the following endpoint by using the `@Get` decorator:

```typescript 
@Get('/')
async getProducts(@Query() filterProductDTO: FilterProductDTO) {
  if (Object.keys(filterProductDTO).length) {
    const filteredProducts = await this.productService.getFilteredProducts(filterProductDTO);
    return filteredProducts;
  } else {
    const allProducts = await this.productService.getAllProducts();
    return allProducts;
  }
}
```

After defining the endpoint, we use `@Query` decorator in the `getProducts()` method and the object from `filter-product.dto.ts` to get the query parameters from a request. 

If the query parameters from a request exist, we use `getFilteredProduct()` method from the product service. If there are no such parameters, we use the regular `getAllProducts()` method instead. 

In the following endpoint, we use the `@Body` decorator to get the needed data from the request body and then pass it to the `addProduct()` method:

```typescript 
@Post('/')
async addProduct(@Body() createProductDTO: CreateProductDTO) {
  const product = await this.productService.addProduct(createProductDTO);
  return product;
}
```

In the next endpoints, we use the `@Param` decorator to get the product ID from the URL:

```typescript 
@Get('/:id')
async getProduct(@Param('id') id: string) {
  const product = await this.productService.getProduct(id);
  if (!product) throw new NotFoundException('Product does not exist!');
  return product;
}

@Put('/:id')
async updateProduct(@Param('id') id: string, @Body() createProductDTO: CreateProductDTO) {
  const product = await this.productService.updateProduct(id, createProductDTO);
  if (!product) throw new NotFoundException('Product does not exist!');
  return product;
}

@Delete('/:id')
async deleteProduct(@Param('id') id: string) {
  const product = await this.productService.deleteProduct(id);
  if (!product) throw new NotFoundException('Product does not exist');
  return product;
}
```

We then use the appropriate method from the product service to get, edit, or delete a product. If a product is not found, we use the `NotFoundException` to throw an error message.

## Creating the user management feature <a name="creating-user-management-feature">

The next feature we need to create for our NestJS ecommerce app is the user management feature.

### Generating our user management resources

For the user management feature, we’ll need only a module and a service. To create them, run the following:

```shell
nest g module user
nest g service user --no-spec 
```

As with the previous feature, we’ll need a schema and DTO.

### Creating a user schema and DTO

In the `user` directory generated by Nest, create a new `schemas` folder. Add a `user.schema.ts` file to this new folder with the following content:

```typescript 
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
// import { Role } from 'src/auth/enums/role.enum';

export type UserDocument = User & Document;

@Schema()
export class User {
  @Prop()
  username: string;

  @Prop()
  email: string;

  @Prop()
  password: string;

/*
  @Prop()
  roles: Role[];
*/
}

export const UserSchema = SchemaFactory.createForClass(User);
```

The commented code towards the end of the block will be used when we implement user authorization. I’ll tell you when to uncomment them later on in this tutorial. 

Next, in the `user` directory, create a new `dtos` folder. Add a `create-user-dto.ts` file in this new folder with the following content:

```typescript 
export class CreateUserDTO {
  username: string;
  email: string;
  password: string;
  roles: string[];
}
```

### Configuring the resources

Open `user.module.ts` and set the schema in the same way as we did with the product feature:

```typescript 
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from './schemas/user.schema';
import { UserService } from './user.service';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])
  ],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule {}
```

In the code above, we are also exporting `UserService` so we can use it in the authentication service later on. 

We’ll also need to install two additional packages: `bcrypt` and `@types/bcrypt`:

```shell
npm install bcrypt
npm install -D @types/bcrypt
```

These packages enable us to keep the password saved, which we will work on in the next section.

### Creating user service methods

Now let’s add the logic for the user management. Open the `user.service.ts` file and replace its content with the following:

```typescript 
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './schemas/user.schema';
import { CreateUserDTO } from './dtos/create-user.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UserService {
  constructor(@InjectModel('User') private readonly userModel: Model<UserDocument>) { }

  async addUser(createUserDTO: CreateUserDTO): Promise<User> {
    const newUser = await this.userModel.create(createUserDTO);
    newUser.password = await bcrypt.hash(newUser.password, 10);
    return newUser.save();
  }

  async findUser(username: string): Promise<User | undefined> {
    const user = await this.userModel.findOne({username: username});
    return user;
  }
}
```

We have added two methods in the code above. The `addUser()` method creates a new user, encrypts the new user’s password by using `bcrypt.hash()`, and then saves the user to the database. 

The `findUser()` method finds a particular user by the `username`.

## Creating user authentication and authorization <a name="creating-user-authentication-authorization">

In this section, we’ll extend the user management feature in our NestJS ecommerce app by adding [user authentication, which verifies the user’s identity](https://docs.nestjs.com/security/authentication), and [user authorization, which defines what the user is allowed to do](https://docs.nestjs.com/security/authorization). 

We’ll use the [well-known Passport library,](https://blog.logrocket.com/using-passport-authentication-node-js/) which provides a big variety of authenticating strategies. Let’s install the necessary packages:

```shell
npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local
```

In the code above, we installed the main `passport` package, the `passport-local` strategy (which implements a simple username and password authentication mechanism), and the Nest passport adapter. We also installed the types for `passport-local`. 

We’ll also need to install also the `dotenv` package for managing environment variables:

```shell 
npm install dotenv
```

Create an `.env` file in the root directory and put the following code inside:

```shell
JWT_SECRET="topsecret"
```

We’ll use this variable later on.

### Generating our user authentication and authorization resources

As usual, let’s start by creating the needed resources for our auth feature:

```shell
nest g module auth
nest g service auth --no-spec 
nest g controller auth --no-spec 
```

### Creating user service methods

Open the `auth.service.ts` file and replace its content with the following:

```typescript 
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(private readonly userService: UserService) {}

  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.userService.findUser(username);
    const isPasswordMatch = await bcrypt.compare(
      password,
      user.password
    );
    if (user && isPasswordMatch) {
      return user;
    }
    return null;
  }
}
```

The code above gives us a user validation method, which retrieves the user and verifies the user’s password.

### Creating a local authentication strategy

In the `auth` directory, create a new `strategies` folder. Add a `local.strategy.ts` file in this new folder with the following content:

```typescript 
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
```

This code does two things. 

First, it calls the `super()` method in the constructor. We can pass an options object here if we need to. We’ll go through an example later. 

Second, we added a `validate()` method, which uses `validateUser()` from the auth service to verify the user.

### Creating an authentication strategy with JWT

Now we’ll create a passport [authentication strategy using JSON Web Tokens (JWT)](https://blog.logrocket.com/jwt-authentication-best-practices/). This will return a JWT for logged users for use in subsequent calls to protected API endpoints. 

Let’s install the necessary packages:

```shell
npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
```

Next, in the `strategies` directory, create a `jwt.strategy.ts` file with the following content:

```typescript 
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import 'dotenv/config'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username, roles: payload.roles };
  }
}
```

In the code above, we set an `options` object with the following properties:

*   `jwtFromRequest` tells the Passport module how JWT will be extracted from the request (in this case, as a bearer token)
*   `ignoreExpiration` set to `false` means the responsibility of ensuring that a JWT has not expired is delegated to the Passport module
*   `secretOrKey` is used to sign the token

The `validate()` method returns a `payload`, which is the JWT decoded as JSON. We then use this payload to return a user object with the necessary properties. 

Now let’s modify the `auth.service.ts` file:

```typescript 
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt'; // 1
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(private readonly userService: UserService, private readonly jwtService: JwtService) {} // 2

  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.userService.findUser(username);
    const isPasswordMatch = await bcrypt.compare(
      password,
      user.password
    );
    if (user && isPasswordMatch) {
      return user;
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user._id, roles: user.roles };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
```

The code above is labeled so you can follow what we did:

*   Imported the `JwtService` (see `//1`)
*   Added `JwtService` to the constructor (see `//2`).

We then used the `login()` method to sign a JWT. 

After all the changes we’ve made, we need to update the `auth.module.ts` in the following manner:

```typescript 
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UserModule } from 'src/user/user.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import 'dotenv/config'

@Module({
  imports: [
    UserModule, 
    PassportModule,     
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '3600s' },
    }),
  ],
  providers: [
    AuthService, 
    LocalStrategy, 
    JwtStrategy
  ],
  controllers: [AuthController],
})
export class AuthModule {}
```

In the code above, we added `UserModule`, `PassportModule`, and `JwtModule` in the `imports` array. 

We also used the `register()` method to provide the necessary options: the `secret` key and `signOptions` object, which set the token expiration to `3600s`, or 1 hour. 

Finally, we added `LocalStrategy` and `JwtStrategy` in the `providers` array.

### Creating local and JWT guards

To use the strategies we’ve just created, we’ll need to create [Guards](https://docs.nestjs.com/guards). 

In `auth` directory, create a new `guards` folder. Add a `local.guard.ts` file to this new folder with the following content:

```typescript 
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
```

Also in the `guards` folder, create a `jwt.guard.ts` file with the following content:

```typescript 
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
```

We’ll see how to use these guards in a minute. But first, let’s create the user authorization functionality.

### Creating user roles management

To implement this feature in our NestJS ecommerce app, we’ll use role-based access control. 

For this feature, we’ll need three files: `role.enum.ts`, `roles.decorator.ts`, and `roles.guard.ts`. Let’s start with the `role.enum.ts` file. 

In the `auth` directory, create a new `enums` folder. Add a `role.enum.ts` file in this new folder with the following content:

```typescript 
export enum Role {
  User = 'user',
  Admin = 'admin',
}
```

This represents the available roles for registered users. 

Now you can go back to the [`user.schema.ts` file we created earlier](https://blog.logrocket.com/how-build-ecommerce-app-nestjs/#creating-user-schema-dto) and uncomment the commented code. 

Next, in the `auth` directory, create a new `decorators` folder. Add a `roles.decorator.ts` file in this new folder with the following content:

```typescript 
import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
```

In the code above, we used `SetMetadata()` to create the decorator. 

Finally, in the `guards` directory, create a `roles.guard.ts` file with the following content:

```typescript 
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}
```

In the code above, we used the [`Reflector` helper class](https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata) to access the route's roles. We also switched the execution context to HTTP with `switchToHttp()` to get the `user` details using `getRequest()`. Finally, we returned the user’s roles.

### Controller methods

Our last step in this section is to create the controller methods. Open the `auth.controller.ts` file and replace its content with the following:

```typescript 
import { Controller, Request, Get, Post, Body, UseGuards } from '@nestjs/common';
import { CreateUserDTO } from 'src/user/dtos/create-user.dto';
import { UserService } from 'src/user/user.service';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { Roles } from './decorators/roles.decorator';
import { Role } from './enums/role.enum';
import { RolesGuard } from './guards/roles.guard';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService, private userService: UserService) {}

  @Post('/register')
  async register(@Body() createUserDTO: CreateUserDTO) {
    const user = await this.userService.addUser(createUserDTO);
    return user;
  }

  @UseGuards(LocalAuthGuard)
  @Post('/login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles(Role.User)
  @Get('/user')
  getProfile(@Request() req) {
    return req.user;
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles(Role.Admin)
  @Get('/admin')
  getDashboard(@Request() req) {
    return req.user;
  }
}
```

We have four endpoints in the code above:

*   POST `auth/register` is used to create a new user
*   POST `auth/login` is used to log in a registered user
    *   To verify the user, we use the `LocalAuthGuard`
*   GET `auth/user` is used to access the user’s profile
    *   We used `JwtGuard` to authenticate the user
    *   We used `RolesGuard` plus `@Roles` decorator to provide the appropriate authorization depending on the user’s roles
*   GET `auth/admin` is used to access the admin dashboard
*   We also used `JwtGuard` and `RolesGuard` as done in the previous endpoint

## Creating the store cart feature for our NestJS ecommerce app <a name="creating-store-cart-feature-nestjs-ecommerce-app">

The last feature we’ll add to our project is a basic cart functionality.

### Creating our store cart resources

Let’s create the resources we need for this next section:

```shell
nest g module cart
nest g service cart --no-spec 
nest g controller cart --no-spec 
```

### Creating the schemas and DTOs

For the store cart feature, we’ll need two schemas: one describing the products in the cart, and one describing the cart itself. 

As usual, in the `cart` directory, create a new `schemas` folder. Add a `item.schema.ts` file in this new folder with the following content:

```typescript 
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, SchemaTypes } from 'mongoose';

export type ItemDocument = Item & Document;

@Schema()
export class Item {
  @Prop({ type: SchemaTypes.ObjectId, ref: 'Product' })
  productId: string;

  @Prop()
  name: string;

  @Prop()
  quantity: number;

  @Prop()
  price: number;

  @Prop()
  subTotalPrice: number; 
}

export const ItemSchema = SchemaFactory.createForClass(Item);
```

In the code above, in the `@Prop` decorator for the `productId` property, we defined an object id schema type and added a reference to the product. This means that we will use the id of the product for the `productId` value. 

The next schema is for the cart. In the `schemas` directory, create a `cart.schema.ts` file with the following content:

```typescript 
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, SchemaTypes } from 'mongoose';
import { Item } from './item.schema';

export type CartDocument = Cart & Document;

@Schema()
export class Cart {
  @Prop({ type: SchemaTypes.ObjectId, ref: 'User' })
  userId: string;

  @Prop()
  items: Item[];

  @Prop()
  totalPrice: number; 
}

export const CartSchema = SchemaFactory.createForClass(Cart);
```

Here, we use the same technique for the `userId` property which will get as a value the user’s id. For the `items` property we use the our `Item` schema to define an array of items with type of `Item`. 

And lastly, let’s create the item DTO. In the `user` directory, create a new `dtos` folder and add an `item.dto.ts` file with the following content:

```typescript 
export class ItemDTO {
  productId: string;
  name: string;
  quantity: number;
  price: number;
}
```

### Configuring the cart module

Before we move to the business logic, we need to add the cart schema to the cart module. Open the `cart.module.ts` file and configure it to use the cart schema as follows:

```typescript 
import { Module } from '@nestjs/common';
import { CartController } from './cart.controller';
import { CartService } from './cart.service';
import { MongooseModule } from '@nestjs/mongoose';
import { CartSchema } from './schemas/cart.schema';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: 'Cart', schema: CartSchema }])
  ],
  controllers: [CartController],
  providers: [CartService]
})
export class CartModule {}
```

### Creating cart service methods

Now let’s create the cart management logic. Open the `cart.service.ts` file and replace its content with the following:

```typescript 
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Cart, CartDocument } from './schemas/cart.schema';
import { ItemDTO } from './dtos/item.dto';

@Injectable()
export class CartService {
  constructor(@InjectModel('Cart') private readonly cartModel: Model<CartDocument>) { }

  async createCart(userId: string, itemDTO: ItemDTO, subTotalPrice: number, totalPrice: number): Promise<Cart> {
    const newCart = await this.cartModel.create({
      userId,
      items: [{ ...itemDTO, subTotalPrice }],
      totalPrice
    });
    return newCart;
  }

  async getCart(userId: string): Promise<CartDocument> {
    const cart = await this.cartModel.findOne({ userId });
    return cart;
  }

  async deleteCart(userId: string): Promise<Cart> {
    const deletedCart = await this.cartModel.findOneAndRemove({ userId });
    return deletedCart;
  }

  private recalculateCart(cart: CartDocument) {
    cart.totalPrice = 0;
    cart.items.forEach(item => {
      cart.totalPrice += (item.quantity * item.price);
    })
  }

  async addItemToCart(userId: string, itemDTO: ItemDTO): Promise<Cart> {
    const { productId, quantity, price } = itemDTO;
    const subTotalPrice = quantity * price;

    const cart = await this.getCart(userId);

    if (cart) {
      const itemIndex = cart.items.findIndex((item) => item.productId == productId);

      if (itemIndex > -1) {
        let item = cart.items[itemIndex];
        item.quantity = Number(item.quantity) + Number(quantity);
        item.subTotalPrice = item.quantity * item.price;

        cart.items[itemIndex] = item;
        this.recalculateCart(cart);
        return cart.save();
      } else {
        cart.items.push({ ...itemDTO, subTotalPrice });
        this.recalculateCart(cart);
        return cart.save();
      }
    } else {
      const newCart = await this.createCart(userId, itemDTO, subTotalPrice, price);
      return newCart;
    }
  }

  async removeItemFromCart(userId: string, productId: string): Promise<any> {
    const cart = await this.getCart(userId);

    const itemIndex = cart.items.findIndex((item) => item.productId == productId);

    if (itemIndex > -1) {
      cart.items.splice(itemIndex, 1);
      return cart.save();
    }
  }
}
```

There are many methods here. Let’s examine them one by one. 

The first one is for creating a new cart for the current user:

```typescript 
async createCart(userId: string, itemDTO: ItemDTO, subTotalPrice: number, totalPrice: number): Promise<Cart> {
  const newCart = await this.cartModel.create({
    userId,
    items: [{ ...itemDTO, subTotalPrice }],
    totalPrice
  });
  return newCart;
}
```

The next two methods are for getting or deleting a particular user’s cart:

```typescript 
async getCart(userId: string): Promise<CartDocument> {
  const cart = await this.cartModel.findOne({ userId });
  return cart;
}

async deleteCart(userId: string): Promise<Cart> {
  const deletedCart = await this.cartModel.findOneAndRemove({ userId });
  return deletedCart;
}
```

The next method is for recalculating the cart total when an item is added or removed, or when an item’s quantity is changed:

```typescript 
private recalculateCart(cart: CartDocument) {
  cart.totalPrice = 0;
  cart.items.forEach(item => {
    cart.totalPrice += (item.quantity * item.price);
  })
}
```

The next method is for adding items to the cart:

```typescript 
async addItemToCart(userId: string, itemDTO: ItemDTO): Promise<Cart> {
  const { productId, quantity, price } = itemDTO;
  const subTotalPrice = quantity * price;

  const cart = await this.getCart(userId);

  if (cart) {
    const itemIndex = cart.items.findIndex((item) => item.productId == productId);

    if (itemIndex > -1) {
      let item = cart.items[itemIndex];
      item.quantity = Number(item.quantity) + Number(quantity);
      item.subTotalPrice = item.quantity * item.price;

      cart.items[itemIndex] = item;
      this.recalculateCart(cart);
      return cart.save();
    } else {
      cart.items.push({ ...itemDTO, subTotalPrice });
      this.recalculateCart(cart);
      return cart.save();
    }
  } else {
    const newCart = await this.createCart(userId, itemDTO, subTotalPrice, price);
    return newCart;
  }
}
```

In the method above, if the cart exists, there are two options:

1.  The product exists, so we need to update its quantity and subtotal price
2.  The product doesn’t exist, so we need to add it

Either way, we need to run the `recalculateCart()` method to update the cart appropriately. If the cart doesn’t exist, we need to create a new one. 

The last method is for removing an item from the cart:

```typescript 
async removeItemFromCart(userId: string, productId: string): Promise<any> {
  const cart = await this.getCart(userId);

  const itemIndex = cart.items.findIndex((item) => item.productId == productId);

  if (itemIndex > -1) {
    cart.items.splice(itemIndex, 1);
    this.recalculateCart(cart);
    return cart.save();
  }
}
```

Similarly to the previous method, in the method above, we run `recalculateCart()` to update the cart correctly after an item is removed.

### Creating cart controller methods

Our final step to finish this NestJS ecommerce app project is to add the cart controller methods. 

Open `cart.controller.ts` file and replace its content with the following:

```typescript 
import { Controller, Post, Body, Request, UseGuards, Delete, NotFoundException, Param } from '@nestjs/common';
import { Roles } from 'src/auth/decorators/roles.decorator';
import { Role } from 'src/auth/enums/role.enum';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { RolesGuard } from 'src/auth/guards/roles.guard';
import { CartService } from './cart.service';
import { ItemDTO } from './dtos/item.dto';

@Controller('cart')
export class CartController {
  constructor(private cartService: CartService) { }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles(Role.User)
  @Post('/')
  async addItemToCart(@Request() req, @Body() itemDTO: ItemDTO) {
    const userId = req.user.userId;
    const cart = await this.cartService.addItemToCart(userId, itemDTO);
    return cart;
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles(Role.User)
  @Delete('/')
  async removeItemFromCart(@Request() req, @Body() { productId }) {
    const userId = req.user.userId;
    const cart = await this.cartService.removeItemFromCart(userId, productId);
    if (!cart) throw new NotFoundException('Item does not exist');
    return cart;
  }

  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles(Role.User)
  @Delete('/:id')
  async deleteCart(@Param('id') userId: string) {
    const cart = await this.cartService.deleteCart(userId);
    if (!cart) throw new NotFoundException('Cart does not exist');
    return cart;
  }
}
```

In the code above, we used `@UseGuards` and `@Roles` decorators for the three methods. This instructs the app that a customer must be logged in and must have a `user` role assigned to add or remove products. 

That’s it. If you have followed along correctly, you should have a basic but fully functional NestJS eccomerce app.

## Conclusion

Phew! This was a pretty long ride. I hope you’ve enjoyed and learned something new about NestJS. 

Despite the detailed explanations needed to explain each step of building this NestJS ecommerce app example, it is pretty basic and can be extended to include even more features. Here are some ideas you can try:

*   Add pagination for the products
*   [Add validation for the received data](https://blog.logrocket.com/understanding-object-validation-joi-nestjs/)
*   Create an order module, in which you can store and manage a particular user’s various orders

As you can see, NestJS is a powerful and flexible server-side framework that can give you a robust and scalable structure for your next projects. If you want to learn more, dive into the [official Nest documentation](https://docs.nestjs.com/) and start building great apps.

---

## [LogRocket](https://logrocket.com/signup/): See the technical and UX reasons for why users don’t complete a step in your ecommerce flow.

[![LogRocket Sign Up](https://blog.logrocket.com/wp-content/uploads/2020/12/insights-step-2-lg.png)](https://logrocket.com/signup/)

[LogRocket](https://logrocket.com/signup/) is like a DVR for web and mobile apps and websites, recording literally everything that happens on your ecommerce app. Instead of guessing why users don’t convert, LogRocket proactively surfaces the root cause of issues that are preventing conversion in your funnel, such as JavaScript errors or dead clicks. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Start proactively monitoring your ecommerce apps — [try for free](https://logrocket.com/signup/).