In this iteration of the tutorial, we shall be creating the customer module for our backend application.

Customers Module

Here’s the customers module file:

src/modules/customers/customers.module.ts view raw
import { Module } from '@nestjs/common';
import { CustomersService } from './customers.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Customer } from './entities/customer.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Customer])],
  providers: [CustomersService],
  exports: [CustomersService],
})
export class CustomersModule {}

Customers Service

Here’s the customers service file:

src/modules/customers/customers.service.ts view raw
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateCustomerDto } from './dto/create-customer.dto';
import { Customer } from './entities/customer.entity';
import * as util from 'util';

@Injectable()
export class CustomersService {
  constructor(
    @InjectRepository(Customer)
    private readonly customersRepository: Repository<Customer>,
  ) {}

  async getByEmail(email: string): Promise<Customer> {
    const customer = await this.customersRepository.findOne({ email });

    if (customer) {
      return customer;
    }

    throw new NotFoundException('Customer with this email does not exist');
  }

  async getById(id: string): Promise<Customer> {
    const customer = await this.customersRepository.findOne({ id });

    if (customer) {
      return customer;
    }

    throw new NotFoundException('Customer with this id does not exist');
  }

  async create(customerData: CreateCustomerDto): Promise<Customer> {
    return new Customer(await this.customersRepository.save(customerData));
  }
}

In the above service, which holds our main application logic, we have three methods, namely: getByEmail, getById and create. As the names of the methods suggest, getByEmail retrieves the customer’s details using their email as a parameter, while the getById method fetches a customer’s details using their ID as a parameter. The create method creates a customer, and we shall see all the above methods in action in the next iteration of the tutorial.

Customer Entity

The customer entity file has remained unchanged from the one in previous tutorials:

src/modules/customers/entities/customer.entity.ts view raw
import { Exclude } from 'class-transformer';
import { Order } from '../../orders/entities/order.entity';
import {
  Column,
  CreateDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity('customers')
export class Customer {
  constructor(intialData: Partial<Customer> = null) {
    if (intialData !== null) {
      Object.assign(this, intialData);
    }
  }

  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'varchar' })
  name: string;

  @Column({ type: 'varchar', unique: true })
  email: string;

  @Column({ name: 'phone_number', type: 'varchar' })
  phoneNumber: string;

  @Exclude()
  @Column({ name: 'password_digest', type: 'varchar' })
  password: string;

  @Exclude()
  confirmPassword: string;

  @CreateDateColumn({
    name: 'created_at',
    type: 'timestamptz',
    default: 'now()',
    readonly: true,
  })
  createdAt: string;

  @UpdateDateColumn({
    name: 'updated_at',
    type: 'timestamptz',
    default: 'now()',
  })
  updatedAt: string;

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

Customer DTOs

Here are the customer DTO (Data Transfer Object) files.

Create Customer DTO:

src/modules/customers/dto/create-customer.dto.ts view raw
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class CreateCustomerDto {
  @IsEmail()
  @IsString()
  @IsNotEmpty()
  email?: string;

  @IsString()
  @IsNotEmpty()
  name?: string;

  @IsString()
  @IsNotEmpty()
  password?: string;

  @IsString()
  @IsNotEmpty()
  confirmPassword?: string;

  @IsString()
  @IsNotEmpty()
  phoneNumber?: string;
}

Update Customer DTO:

src/modules/customers/dto/update-customer.dto.ts view raw
import { PartialType } from '@nestjs/mapped-types';
import { CreateCustomerDto } from './create-customer.dto';

export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {}

Login Customer DTO:

src/modules/customers/dto/login-customer.dto.ts view raw
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class LoginCustomerDto {
  @IsString()
  @IsNotEmpty()
  @IsEmail()
  email: string;

  @IsString()
  @IsNotEmpty()
  password: string;
}

In Nest.js, DTOs are used to validate incoming data from a request before being proccessed by the Nest.js framework. They are usually generated by default when you create new resouces using the Nest.js CLI. The first two are for validating a customer details before creation and update respectively, while the last DTO is used when a customer logs in to our application, and will be used in a later tutorial. We’ll leave it in place for now.

Unit Tests

Since we have not created any controllers, we shall therefore have no need for an end to end test within this module. Instead, we shall create unit tests for our service logic, which look as follows:

src/modules/customers/spec/customers.service.spec.ts view raw
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { CustomersService } from '../customers.service';
import { CreateCustomerDto } from '../dto/create-customer.dto';
import { Customer } from '../entities/customer.entity';

describe('CustomersService', () => {
  let service: CustomersService;

  beforeEach(async () => {
    const createMock = jest.fn((dto: any) => {
      return dto;
    });

    const saveMock = jest.fn((dto: any) => {
      return dto;
    });

    const findOneMock = jest.fn((dto: any) => {
      return dto;
    });

    const MockRepository = jest.fn().mockImplementation(() => {
      return {
        create: createMock,
        save: saveMock,
        findOne: findOneMock,
      };
    });

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CustomersService,
        {
          provide: getRepositoryToken(Customer),
          useClass: MockRepository,
        },
      ],
    }).compile();

    service = module.get<CustomersService>(CustomersService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('when getting a customer by email', () => {
    const customer = new Customer({});
    const testEmail = 'test@test.com';

    describe('and the customer is matched', () => {
      it('should return the customer', async () => {
        jest
          .spyOn(service, 'getByEmail')
          .mockImplementation(async () => customer);
        expect(await service.getByEmail(testEmail)).toBe(customer);
      });
    });

    describe('and the customer is not matched', () => {
      it('should throw an error', async () => {
        jest
          .spyOn(service, 'getByEmail')
          .mockRejectedValue(
            new Error('Customer with this email does not exist'),
          );
        await expect(service.getByEmail(testEmail)).rejects.toThrow();
      });
    });
  });

  describe('when getting a customer by id', () => {
    const customer = new Customer({});
    const testId = '59f78b6b-b1fb-4cf3-ade1-608f37a9d3fa';

    describe('and the customer is matched', () => {
      it('should return the customer', async () => {
        jest.spyOn(service, 'getById').mockImplementation(async () => customer);
        expect(await service.getById(testId)).toBe(customer);
      });
    });

    describe('and the customer is not matched', () => {
      it('should throw an error', async () => {
        jest
          .spyOn(service, 'getById')
          .mockRejectedValue(new Error('Customer with this id does not exist'));
        await expect(service.getById(testId)).rejects.toThrow();
      });
    });
  });

  describe('create', () => {
    it('should return a new customer', async () => {
      let customer: Promise<Customer>;
      const testCreateCustomerDto = new CreateCustomerDto();
      jest.spyOn(service, 'create').mockImplementation(() => customer);
      expect(await service.create(testCreateCustomerDto)).toBe(customer);
    });
  });
});

We are using the Jest testing framework, which comes bundled with Nest.js. The describe blocks explain what the specific unit test is required to perform.

When we run our tests, we should get an all green:

Customer Spec