Published
- 18 min read
NestJS Microservice: Xây dựng các microservice Auth, Order, Product với gRPC - Part 2/2
Trong phần 1 của loạt bài, Mình đã hướng dẫn chi tiết cách xây dựng API Gateway cho hệ thống microservices sử dụng NestJS và gRPC, giúp quản lý luồng dữ liệu và tương tác giữa các dịch vụ một cách hiệu quả. các bạn có thể xem lại tại đây.
Mình sử dụng database Postgresql để lưu trữ dữ liệu nên các bạn cần cài Postgresql trước nhé!
Xây dựng service Authentication
Chúng ta sẽ bắt đầu bằng việc khởi tạo project Authentication Service bằng NestJS CLI
nest new auth-svc -p npm
Copy folder proto ở project api-gateway
vào folder của dự án
Cài đặt các packages
npm i @nestjs/microservices @nestjs/typeorm @nestjs/jwt @nestjs/passport passport passport-jwt typeorm pg class-transformer class-validator bcryptjs
npm i -D @types/node @types/passport-jwt ts-proto
Setup cấu trúc dự án
Nest g mo auth && Nest g co auth --no-spec
mkdir src/auth/filter && mkdir src/auth/service && mkdir src/auth/strategy
touch src/auth/filter/http-Exception.filter.ts
touch src/auth/service/auth.service.ts
touch src/auth/service/jwt.service.ts
touch src/auth/strategy/jwt.strategy.ts
touch src/auth/auth.dto.ts
touch src/auth/auth.entity.ts
Thêm script vào package.json
để generate ra file .pb.ts
tương tự như chúng ta đã làm ở api-gateway
"proto:auth": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./proto --ts_proto_out=src/auth/ proto/auth.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
Chạy lệnh sau:
npm run proto:auth
Project của chúng ta sẽ như thế này
HTTP Exception Filter
Trong quá trình xây dựng hệ thống gRPC với NestJS, chúng ta sẽ sử dụng các đối tượng truyền dữ liệu (Data-Transfer-Objects - DTO) để xác thực payload của các yêu cầu. Khi sử dụng gói class-validator, nó sẽ tạo ra các ngoại lệ HTTP nếu payload không hợp lệ. Tuy nhiên, vì mục tiêu của chúng ta là giữ cho dịch vụ gRPC của mình hoạt động trơn tru mà không gửi ngoại lệ HTTP, chúng ta cần chuyển đổi các ngoại lệ này thành phản hồi gRPC chuẩn. Điều này giúp đảm bảo rằng các lỗi xác thực được xử lý một cách liền mạch, cung cấp các phản hồi dễ hiểu và không gây gián đoạn cho hệ thống microservices của chúng ta.
Thêm code vào src/auth/filter/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { HttpArgumentsHost } from '@nestjs/common/interfaces';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx: HttpArgumentsHost = host.switchToHttp();
const res: Response = ctx.getResponse<Response>();
const req: Request = ctx.getRequest<Request>();
const status: HttpStatus = exception.getStatus();
if (status === HttpStatus.BAD_REQUEST) {
const res: any = exception.getResponse();
return { status, error: res.message };
}
res.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: req.url,
});
}
}
Auth DTO
Thêm code vào src/auth/auth.dto.ts
import { IsEmail, IsString, MinLength } from '@nestjs/class-validator';
import { LoginRequest, RegisterRequest, ValidateRequest } from './auth.pb';
export class LoginRequestDto implements LoginRequest {
@IsEmail()
public readonly email: string;
@IsString()
public readonly password: string;
}
export class RegisterRequestDto implements RegisterRequest {
@IsEmail()
public readonly email: string;
@IsString()
@MinLength(8)
public readonly password: string;
}
export class ValidateRequestDto implements ValidateRequest {
@IsString()
public readonly token: string;
}
Auth Entity
Thêm code vào src/auth/auth.entity.ts
import { Exclude } from '@nestjs/class-transformer';
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Auth extends BaseEntity {
@PrimaryGeneratedColumn()
public id!: number;
@Column({ type: 'varchar' })
public email!: string;
@Exclude()
@Column({ type: 'varchar' })
public password!: string;
}
JWT Service
Thêm code vào src/auth/service/jwt.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService as Jwt } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Auth } from '../auth.entity';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class JwtService {
@InjectRepository(Auth)
private readonly repository: Repository<Auth>;
private readonly jwt: Jwt;
constructor(jwt: Jwt) {
this.jwt = jwt;
}
// Decoding the JWT Token
public async decode(token: string): Promise<unknown> {
return this.jwt.decode(token, null);
}
// Get User by User ID we get from decode()
public async validateUser(decoded: any): Promise<Auth> {
return this.repository.findOne({ where: { id: decoded.id } });
}
// Generate JWT Token
public generateToken(auth: Auth): string {
return this.jwt.sign({ id: auth.id, email: auth.email });
}
// Validate User's password
public isPasswordValid(password: string, userPassword: string): boolean {
return bcrypt.compareSync(password, userPassword);
}
// Encode User's password
public encodePassword(password: string): string {
const salt: string = bcrypt.genSaltSync(10);
return bcrypt.hashSync(password, salt);
}
// Validate JWT Token, throw forbidden error if JWT Token is invalid
public async verify(token: string): Promise<any> {
try {
return this.jwt.verify(token);
} catch (err) { }
}
}
JWT Strategy
Thêm code vào src/auth/strategy/jwt.strategy.ts
import { Injectable, Inject } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Auth } from '../auth.entity';
import { JwtService } from '../service/jwt.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
@Inject(JwtService)
private readonly jwtService: JwtService;
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'dev',
ignoreExpiration: true,
});
}
private validate(token: string): Promise<Auth | never> {
return this.jwtService.validateUser(token);
}
}
Auth Service
Thêm code vào src/auth/service/auth.service.ts
import { HttpStatus, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JwtService } from './jwt.service';
import {
RegisterRequestDto,
LoginRequestDto,
ValidateRequestDto,
} from '../auth.dto';
import { Auth } from '../auth.entity';
import { LoginResponse, RegisterResponse, ValidateResponse } from '../auth.pb';
@Injectable()
export class AuthService {
@InjectRepository(Auth)
private readonly repository: Repository<Auth>;
@Inject(JwtService)
private readonly jwtService: JwtService;
public async register({
email,
password,
}: RegisterRequestDto): Promise<RegisterResponse> {
let auth: Auth = await this.repository.findOne({ where: { email } });
if (auth) {
return { status: HttpStatus.CONFLICT, error: ['E-Mail already exists'] };
}
auth = new Auth();
auth.email = email;
auth.password = this.jwtService.encodePassword(password);
await this.repository.save(auth);
return { status: HttpStatus.CREATED, error: null };
}
public async login({
email,
password,
}: LoginRequestDto): Promise<LoginResponse> {
const auth: Auth = await this.repository.findOne({ where: { email } });
if (!auth) {
return {
status: HttpStatus.NOT_FOUND,
error: ['E-Mail not found'],
token: null,
};
}
const isPasswordValid: boolean = this.jwtService.isPasswordValid(
password,
auth.password,
);
if (!isPasswordValid) {
return {
status: HttpStatus.NOT_FOUND,
error: ['Password wrong'],
token: null,
};
}
const token: string = this.jwtService.generateToken(auth);
return { token, status: HttpStatus.OK, error: null };
}
public async validate({
token,
}: ValidateRequestDto): Promise<ValidateResponse> {
const decoded: Auth = await this.jwtService.verify(token);
if (!decoded) {
return {
status: HttpStatus.FORBIDDEN,
error: ['Token is invalid'],
userId: null,
};
}
const auth: Auth = await this.jwtService.validateUser(decoded);
if (!auth) {
return {
status: HttpStatus.CONFLICT,
error: ['User not found'],
userId: null,
};
}
return { status: HttpStatus.OK, error: null, userId: decoded.id };
}
}
Auth Controller
Thêm code vào src/auth/auth.controller.ts
import { Controller, Inject } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import {
LoginRequestDto,
RegisterRequestDto,
ValidateRequestDto,
} from './auth.dto';
import {
AUTH_SERVICE_NAME,
RegisterResponse,
LoginResponse,
ValidateResponse,
} from './auth.pb';
import { AuthService } from './service/auth.service';
@Controller()
export class AuthController {
@Inject(AuthService)
private readonly service: AuthService;
@GrpcMethod(AUTH_SERVICE_NAME, 'Register')
private register(payload: RegisterRequestDto): Promise<RegisterResponse> {
return this.service.register(payload);
}
@GrpcMethod(AUTH_SERVICE_NAME, 'Login')
private login(payload: LoginRequestDto): Promise<LoginResponse> {
return this.service.login(payload);
}
@GrpcMethod(AUTH_SERVICE_NAME, 'Validate')
private validate(payload: ValidateRequestDto): Promise<ValidateResponse> {
return this.service.validate(payload);
}
}
Auth Module
Thay đổi code ở src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { Auth } from './auth.entity';
import { AuthService } from './service/auth.service';
import { JwtService } from './service/jwt.service';
import { JwtStrategy } from './strategy/jwt.strategy';
@Module({
imports: [
JwtModule.register({
secret: 'dev',
signOptions: { expiresIn: '365d' },
}),
TypeOrmModule.forFeature([Auth]),
],
controllers: [AuthController],
providers: [AuthService, JwtService, JwtStrategy],
})
export class AuthModule { }
App Module
Thay đổi code ở src/app.module.ts
và tạo database với tên micro_auth
nha, các bạn có thể sử dụng env config để thay thế các giá trị của database
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
database: 'micro_auth',
username: 'admin',
password: 'admin',
entities: ['dist/**/*.entity.{ts,js}'],
synchronize: true, // never true in production!
}),
AuthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
Main
Thay đổi code ở main.ts
import { INestMicroservice, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';
import { protobufPackage } from './auth/auth.pb';
import { HttpExceptionFilter } from './auth/filter/http-Exception.filter';
async function bootstrap() {
const app: INestMicroservice = await NestFactory.createMicroservice(
AppModule,
{
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50051',
package: protobufPackage,
protoPath: join('proto/auth.proto'),
},
},
);
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.listen();
}
bootstrap();
Xây dựng service Product
Trước tiên cũng sẽ bắt đầu bằng việc khởi tạo project Product Service bằng NestJS CLI
nest new product-svc -p npm
Cài đặt các package
npm i @nestjs/microservices @grpc/grpc-js @grpc/proto-loader @nestjs/typeorm typeorm pg class-transformer class-validator
npm i -D @types/node ts-proto
Setup cấu trúc dự án
Copy folder proto ở project api-gateway
vào folder của dự án
Nest g mo product && Nest g co product --no-spec && Nest g s product --no-spec
mkdir src/product/entity
touch src/product/product.dto.ts
touch src/product/entity/product.entity.ts
touch src/product/entity/stock-decrease-log.entity.ts
Thêm script vào package.json
để generate ra file .pb.ts
tương tự như Authentication service
"proto:product": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./proto --ts_proto_out=src/product/ proto/product.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
Chạy lệnh sau:
npm run proto:product
Project của chúng ta sẽ trông như sau
Product Entity
Thêm code vào src/product/entity/product.entity.ts
import {
BaseEntity,
Column,
Entity,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { StockDecreaseLog } from './stock-decrease-log.entity';
@Entity()
export class Product extends BaseEntity {
@PrimaryGeneratedColumn()
public id!: number;
@Column({ type: 'varchar' })
public name!: string;
@Column({ type: 'varchar' })
public sku!: string;
@Column({ type: 'integer' })
public stock!: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
public price!: number;
/*
* One-To-Many Relationships
*/
@OneToMany(
() => StockDecreaseLog,
(stockDecreaseLog) => stockDecreaseLog.product,
)
public stockDecreaseLogs: StockDecreaseLog[];
}
StockDecreaseLog Entity
Để đảm bảo tính nhất quán và tránh các lỗi không mong muốn khi giảm hàng tồn kho, chúng ta cần tạo Entity StockDecreaseLog
. Thực thể này sẽ ghi lại mọi thao tác giảm hàng tồn kho, liên kết với Order ID
và Product ID
tương ứng. Việc này giúp chúng ta theo dõi chi tiết từng hành động giảm hàng và ngăn chặn các trường hợp giảm hàng trùng lặp do cùng một đơn hàng. Bằng cách lưu trữ thông tin này, hệ thống có thể duy trì tính toàn vẹn của dữ liệu ngay cả khi xảy ra các lỗi không thể đoán trước, đảm bảo rằng mỗi yêu cầu giảm hàng chỉ được thực hiện một lần duy nhất.
Thêm code vào src/product/entity/stock-decrease-log.entity.ts
import {
BaseEntity,
Column,
Entity,
PrimaryGeneratedColumn,
ManyToOne,
} from 'typeorm';
import { Product } from './product.entity';
@Entity()
export class StockDecreaseLog extends BaseEntity {
@PrimaryGeneratedColumn()
public id!: number;
@Column({ type: 'integer' })
public quantity!: number;
/*
* Relation IDs
*/
@Column({ type: 'integer' })
public orderId!: number;
/*
* Many-To-One Relationships
*/
@ManyToOne(() => Product, (product) => product.stockDecreaseLogs)
public product: Product;
}
Product DTO
Hãy thay đổi code ở src/product/product.dto.ts
import { IsNotEmpty, IsNumber, IsString } from '@nestjs/class-validator';
import {
CreateProductRequest,
DecreaseStockRequest,
FindOneRequest,
} from './product.pb';
export class FindOneRequestDto implements FindOneRequest {
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly id: number;
}
export class CreateProductRequestDto implements CreateProductRequest {
@IsString()
@IsNotEmpty()
public readonly name: string;
@IsString()
@IsNotEmpty()
public readonly sku: string;
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly stock: number;
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly price: number;
}
export class DecreaseStockRequestDto implements DecreaseStockRequest {
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly id: number;
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly orderId: number;
@IsNumber({ allowInfinity: false, allowNaN: false })
public readonly quantity: number;
}
Product Service
Thay đổi code ở src/product/product.service.ts
import { HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entity/product.entity';
import {
CreateProductRequestDto,
DecreaseStockRequestDto,
FindOneRequestDto,
} from './product.dto';
import {
CreateProductResponse,
DecreaseStockResponse,
FindOneResponse,
} from './product.pb';
import { StockDecreaseLog } from './entity/stock-decrease-log.entity';
@Injectable()
export class ProductService {
@InjectRepository(Product)
private readonly repository: Repository<Product>;
@InjectRepository(StockDecreaseLog)
private readonly decreaseLogRepository: Repository<StockDecreaseLog>;
public async findOne({ id }: FindOneRequestDto): Promise<FindOneResponse> {
const product: Product = await this.repository.findOne({ where: { id } });
if (!product) {
return {
data: null,
error: ['Product not found'],
status: HttpStatus.NOT_FOUND,
};
}
return { data: product, error: null, status: HttpStatus.OK };
}
public async createProduct(
payload: CreateProductRequestDto,
): Promise<CreateProductResponse> {
const product: Product = new Product();
product.name = payload.name;
product.sku = payload.sku;
product.stock = payload.stock;
product.price = payload.price;
await this.repository.save(product);
return { id: product.id, error: null, status: HttpStatus.OK };
}
public async decreaseStock({
id,
orderId,
quantity,
}: DecreaseStockRequestDto): Promise<DecreaseStockResponse> {
const product: Product = await this.repository.findOne({
select: ['id', 'stock'],
where: { id },
});
if (!product) {
return { error: ['Product not found'], status: HttpStatus.NOT_FOUND };
} else if (product.stock <= 0) {
return { error: ['Stock too low'], status: HttpStatus.CONFLICT };
}
const isAlreadyDecreased: number = await this.decreaseLogRepository.count({
where: { orderId },
});
if (isAlreadyDecreased) {
// Idempotence
return {
error: ['Stock already decreased'],
status: HttpStatus.CONFLICT,
};
}
await this.repository.update(product.id, {
stock: product.stock - quantity,
});
await this.decreaseLogRepository.insert({ product, orderId, quantity });
return { error: null, status: HttpStatus.OK };
}
}
Product Controller
Thay đổi file src/product/product.controller.ts
import { Controller, Inject } from '@nestjs/common';
import {
CreateProductRequestDto,
DecreaseStockRequestDto,
FindOneRequestDto,
} from './product.dto';
import {
CreateProductResponse,
DecreaseStockResponse,
FindOneResponse,
PRODUCT_SERVICE_NAME,
} from './product.pb';
import { ProductService } from './product.service';
import { GrpcMethod } from '@nestjs/microservices';
@Controller()
export class ProductController {
@Inject(ProductService)
private readonly service: ProductService;
@GrpcMethod(PRODUCT_SERVICE_NAME, 'FindOne')
private findOne(payload: FindOneRequestDto): Promise<FindOneResponse> {
return this.service.findOne(payload);
}
@GrpcMethod(PRODUCT_SERVICE_NAME, 'CreateProduct')
private createProduct(
payload: CreateProductRequestDto,
): Promise<CreateProductResponse> {
return this.service.createProduct(payload);
}
@GrpcMethod(PRODUCT_SERVICE_NAME, 'DecreaseStock')
private decreaseStock(
payload: DecreaseStockRequestDto,
): Promise<DecreaseStockResponse> {
return this.service.decreaseStock(payload);
}
}
Product Module
Thay đổi file src/product/product.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';
import { Product } from './entity/product.entity';
import { StockDecreaseLog } from './entity/stock-decrease-log.entity';
@Module({
imports: [TypeOrmModule.forFeature([Product, StockDecreaseLog])],
controllers: [ProductController],
providers: [ProductService],
})
export class ProductModule { }
App Module
Thay đổi nội dung src/app.module.ts
và tạo database với tên micro_product
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductModule } from './product/product.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
database: 'micro_product',
username: 'admin',
password: 'admin',
entities: ['dist/**/*.entity.{ts,js}'],
synchronize: true, // never true in production!
}),
ProductModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
Main
Thay đổi nội dung main.ts
import { INestMicroservice, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';
import { protobufPackage } from './product/product.pb';
async function bootstrap() {
const app: INestMicroservice = await NestFactory.createMicroservice(
AppModule,
{
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50053',
package: protobufPackage,
protoPath: join('proto/product.proto'),
},
},
);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.listen();
}
bootstrap();
Xây dựng service Order
Trước tiên cũng sẽ bắt đầu bằng việc khởi tạo project Order Service bằng NestJS CLI
nest new order-svc -p npm
Cài đặt các package
npm i @nestjs/microservices @grpc/grpc-js @grpc/proto-loader @nestjs/typeorm typeorm pg class-transformer class-validator
npm i -D @types/node ts-proto
Setup cấu trúc dự án
Copy folder proto ở project api-gateway
vào folder của dự án
Nest g mo order && Nest g co order --no-spec && Nest g s order --no-spec
mkdir src/order/proto
touch src/order/order.dto.ts
touch src/order/order.entity.ts
Thêm script vào package.json
để generate ra file .pb.ts
tương tự như Product service
"proto:order": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./proto --ts_proto_out=src/order/proto/ proto/order.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
"proto:product": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./proto --ts_proto_out=src/order/proto/ proto/product.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
Chạy script:
npm run proto:order
npm run proto:product
Service Order có sử dụng dữ liệu của service Product thông qua kết nối gRPC nên cần generate ra file product.pb.ts
Project của chúng ta sẽ như sau
Order Entity
Hãy thêm code vào src/order/order.entity.ts
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Order extends BaseEntity {
@PrimaryGeneratedColumn()
public id!: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
public price!: number;
@Column({ type: 'integer' })
public quantity!: number;
/*
* Relation IDs
*/
@Column({ type: 'integer' })
public productId!: number;
@Column({ type: 'integer' })
public userId!: number;
}
Order DTO
Hãy thêm code vào src/order/order.dto.ts
import { IsNumber, Min } from '@nestjs/class-validator';
import { CreateOrderRequest } from './proto/order.pb';
export class CreateOrderRequestDto implements CreateOrderRequest {
@IsNumber()
public productId: number;
@IsNumber()
@Min(1)
public quantity: number;
@IsNumber()
public userId: number;
}
Order Service
Order Service trong hệ thống của chúng ta có một vai trò đặc biệt, vì đây là lần đầu tiên chúng ta thực hiện cuộc gọi từ một microservice này đến một microservice khác. Cụ thể, Order Service sẽ tương tác với Product Microservice
hai lần trong quá trình xử lý đơn hàng. Đầu tiên, nó sẽ kiểm tra tính khả dụng của sản phẩm bằng cách xác nhận rằng sản phẩm vẫn còn tồn kho. Sau khi đảm bảo sản phẩm tồn tại và có đủ hàng, Order Service sẽ tiếp tục gọi đến Product Microservice để giảm số lượng hàng tồn kho tương ứng với đơn đặt hàng. Bằng cách này, chúng ta không chỉ đảm bảo rằng đơn hàng được xử lý chính xác mà còn duy trì sự nhất quán trong việc quản lý tồn kho.
Thay đổi file src/order/order.service.ts
import { HttpStatus, Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ClientGrpc } from '@nestjs/microservices';
import { Repository } from 'typeorm';
import { firstValueFrom } from 'rxjs';
import { Order } from './order.entity';
import {
FindOneResponse,
DecreaseStockResponse,
ProductServiceClient,
PRODUCT_SERVICE_NAME,
} from './proto/product.pb';
import { CreateOrderRequest, CreateOrderResponse } from './proto/order.pb';
@Injectable()
export class OrderService implements OnModuleInit {
private productSvc: ProductServiceClient;
@Inject(PRODUCT_SERVICE_NAME)
private readonly client: ClientGrpc;
@InjectRepository(Order)
private readonly repository: Repository<Order>;
public onModuleInit(): void {
this.productSvc =
this.client.getService<ProductServiceClient>(PRODUCT_SERVICE_NAME);
}
public async createOrder(
data: CreateOrderRequest,
): Promise<CreateOrderResponse> {
const product: FindOneResponse = await firstValueFrom(
this.productSvc.findOne({ id: data.productId }),
);
if (product.status >= HttpStatus.NOT_FOUND) {
return { id: null, error: ['Product not found'], status: product.status };
} else if (product.data.stock < data.quantity) {
return {
id: null,
error: ['Stock too less'],
status: HttpStatus.CONFLICT,
};
}
const order: Order = new Order();
order.price = product.data.price;
order.quantity = data.quantity;
order.productId = product.data.id;
order.userId = data.userId;
await this.repository.save(order);
const decreasedStockData: DecreaseStockResponse = await firstValueFrom(
this.productSvc.decreaseStock({
id: data.productId,
orderId: order.id,
quantity: data.quantity,
}),
);
if (decreasedStockData.status === HttpStatus.CONFLICT) {
// deleting order if decreaseStock fails
await this.repository.delete(order.id);
return {
id: null,
error: decreasedStockData.error,
status: HttpStatus.CONFLICT,
};
}
return { id: order.id, error: null, status: HttpStatus.OK };
}
}
Order Controller
Thay đổi src/order/order.controller.ts
import { Controller, Inject } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { OrderService } from './order.service';
import { ORDER_SERVICE_NAME, CreateOrderResponse } from './proto/order.pb';
import { CreateOrderRequestDto } from './order.dto';
@Controller('order')
export class OrderController {
@Inject(OrderService)
private readonly service: OrderService;
@GrpcMethod(ORDER_SERVICE_NAME, 'CreateOrder')
private async createOrder(
data: CreateOrderRequestDto,
): Promise<CreateOrderResponse> {
return this.service.createOrder(data);
}
}
Order Module
Thay đổi src/order/order.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OrderController } from './order.controller';
import { Order } from './order.entity';
import { OrderService } from './order.service';
import { PRODUCT_SERVICE_NAME, PRODUCT_PACKAGE_NAME } from './proto/product.pb';
@Module({
imports: [
ClientsModule.register([
{
name: PRODUCT_SERVICE_NAME,
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50053',
package: PRODUCT_PACKAGE_NAME,
protoPath: 'proto/product.proto',
},
},
]),
TypeOrmModule.forFeature([Order]),
],
controllers: [OrderController],
providers: [OrderService],
})
export class OrderModule { }
App Module
Thay đổi file src/app.module.ts
và tạo database với tên micro_order
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { OrderModule } from './order/order.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
database: 'micro_order',
username: 'admin',
password: 'admin',
entities: ['dist/**/*.entity.{ts,js}'],
synchronize: true, // never true in production!
}),
OrderModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
main.ts
Thay đổi main.ts
import { INestMicroservice, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';
import { protobufPackage } from './order/proto/order.pb';
async function bootstrap() {
const app: INestMicroservice = await NestFactory.createMicroservice(
AppModule,
{
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50052',
package: protobufPackage,
protoPath: join('proto/order.proto'),
},
},
);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.listen();
}
bootstrap();
Tất cả code đã hoàn thành mình giải thích 1 chút nhé.
Trong hệ thống microservices của chúng ta, mỗi dịch vụ đều có vai trò cụ thể và chạy trên các cổng khác nhau: Authentication service
tại cổng 50051
, Order service
tại cổng 50052
, và Product service
tại cổng 50053
. Mỗi dịch vụ thực hiện các nhiệm vụ riêng biệt và đều được quản lý thông qua API Gateway
. Tuy nhiên, sự linh hoạt không dừng lại ở đó - các dịch vụ này cũng có khả năng giao tiếp trực tiếp với nhau. Chẳng hạn, Order service
có thể gọi đến Product service
để kiểm tra tồn kho hoặc cập nhật số lượng sản phẩm một cách hiệu quả và nhanh chóng thông qua giao thức gRPC
.
Nhờ kiến trúc này, chúng ta có thể triển khai từng dịch vụ trên các máy chủ riêng biệt, dễ dàng mở rộng hệ thống khi cần mà không gây ra gián đoạn cho các dịch vụ khác. Điều này không chỉ tăng cường hiệu suất và khả năng mở rộng mà còn đảm bảo tính độc lập và tính liên tục của mỗi dịch vụ trong hệ thống microservices.
Kiểm tra và chạy thử
Chúng ta vào các service API Gateway, Authentication, Order và Product và run tất cả lên
// API Gateway
pnpm run start:dev
// Authentication, product, Order
npm run start:dev
Tiếp theo chúng ta vào Postman và chạy thử API Register
Như vậy đã tạo được account, kiểm tra lại database micro_auth để xem record đã được thêm vào
Tương tự với các API: Login, Create product, Find product và Create order các bạn có thể tham khảo ở Postman collection mình đã tạo sẵn ^^
Trên đây là toàn bộ demo về xây dựng hệ thống Microservice API Gateway với NestJS và kết nối gRPC. Trong thực tế, còn nhiều yếu tố bổ sung như Cache, Message Queue, Scale service, Monitoring và Logging, Security…
Hy vọng các bạn sẽ yêu thích series này và nếu có cơ hội, mình sẽ viết một bài về tạo service gửi email và giao tiếp thông qua RabbitMQ (message queue).
Code tham khảo: https://github.com/duy-lee/nestjs-grpc-microservice