This article is Part 5 in a 10 Part Series.
In this iteration of the tutorial, we shall be creating the customer module for our backend application. In case of any issues, you can refer to my Github repository here .
Customers Module
Here’s the customers module file:
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:
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:
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:
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:
import { PartialType } from ' @nestjs/mapped-types ' ;
import { CreateCustomerDto } from ' ./create-customer.dto ' ;
export class UpdateCustomerDto extends PartialType ( CreateCustomerDto ) {}
Login Customer DTO:
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 in the request body before creating and updating a customer 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:
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: