NestJS: Microservices với gRPC, API Gateway, và Authentication — Part 1/2
NestJS: Microservices với gRPC, API Gateway, và Authentication — Part 1/2
Hôm nay, tôi muốn giới thiệu với bạn về Microservices trong NestJS (TypeScript) kết hợp với framework gRPC của Google và API Gateway để xử lý các yêu cầu HTTP đến và xác thực dựa trên JWT. Bài viết này rất dài, vì vậy tôi đã chia nó thành 2 phần.
- Introduction, Preparation, Databases, Shared Proto Project
- Authentication Microservice, Product Microservice, Order Microservice
Application Infrastructure
Trong bài viết này, tôi quyết định viết mã cho một dự án Microservice thương mại điện tử đơn giản với API Gateway quản lý các yêu cầu HTTP đến và chuyển tiếp chúng đến các Microservices, tổng cộng sẽ có 3 service.
- Service đầu tiên sẽ là xác thực, nơi người dùng có thể đăng ký và đăng nhập trong khi chúng tôi xác thực quyền của yêu cầu.
- Service thứ hai sẽ xử lý sản phẩm, tạo sản phẩm mới và tìm sản phẩm dựa trên ID của nó.
- Service thứ ba sẽ xử lý các đơn hàng cho ứng dụng thương mại điện tử nhỏ của chúng ta.
Bạn sẽ thấy rằng mỗi dịch vụ này đơn giản nhất có thể để giới thiệu bạn về những điều cơ bản của microservices trong NestJS và TypeScript, mà không làm phức tạp ứng dụng của chúng ta.
Trước khi chúng ta bắt đầu với hướng dẫn này, hãy nói ngắn gọn về các framework và khái niệm mà chúng ta sẽ sử dụng ở đây.
NestJS là gì?
NestJS là một framework để xây dựng các ứng dụng web Node.js hiệu quả và có khả năng mở rộng. Nó sử dụng JavaScript hiện đại và được xây dựng bằng TypeScript. Nếu bạn phát triển một API bằng TypeScript, thì NestJS là lựa chọn lý tưởng! Nó được lấy cảm hứng mạnh mẽ từ Spring và Angular.
gRPC là gì?
gRPC là một framework RPC hiện đại, mã nguồn mở, hiệu suất cao có thể chạy trong mọi môi trường. Nó có thể kết nối các dịch vụ trong và giữa các trung tâm dữ liệu một cách hiệu quả với sự hỗ trợ có thể cắm được cho cân bằng tải, theo dõi, kiểm tra sức khỏe và xác thực.
API Gateway là gì?
API Gateway là một điểm đầu vào cho tất cả các khách hàng, trong trường hợp của chúng ta, cho tất cả các yêu cầu từ khách hàng dựa trên HTTP. Yêu cầu chỉ đơn giản được chuyển tiếp/định tuyến đến service phù hợp. Nó xử lý các yêu cầu khác bằng cách phân tán đến nhiều service.
Các yêu cầu tiên quyết
Bạn cần có hiểu biết cơ bản về TypeScript, RPC, Git (+ Github), PostgreSQL mà bạn đã cài đặt trên máy của mình. Tôi sẽ chọn Visual Studio Code làm trình soạn thảo mã nguồn của mình. Bạn có thể sử dụng bất kỳ trình soạn thảo nào mà bạn ưa thích.
Database
Đầu tiên, chúng ta cần tạo 3 cơ sở dữ liệu PostgreSQL. Điều này là do chúng ta sẽ tuân theo mô hình Cơ sở dữ liệu cho mỗi service. Mỗi microservice sẽ có cơ sở dữ liệu riêng của nó để độc lập khi quản lý dữ liệu.
psql postgres
CREATE DATABASE micro_auth;
CREATE DATABASE micro_product;
CREATE DATABASE micro_order;
Tạo project
Tiếp tục với NestJS. Chúng ta sẽ cài đặt NestJS CLI.
npm i -g @nestjs/cli
Chúng ta khởi tạo 4 dự án NestJS mới bằng CLI của nó. Ngoài ra, chúng ta tạo một dự án cho các tệp proto mà chúng ta sẽ chia sẻ qua Github.
mkdir grpc-nest-proto
nest new grpc-nest-api-gateway -p npm
nest new grpc-nest-auth-svc -p npm
nest new grpc-nest-product-svc -p npm
nest new grpc-nest-order-svc -p npm
Shared Proto Repository
Đầu tiên chúng ta cần khởi tạo project grpc-proto
cd grpc-nest-proto
npm init --y
git init
mkdir proto
touch proto/auth.proto && touch proto/product.proto && touch proto/order.proto
code .
project của chúng ta sẽ trông như này
Sau đó, chúng ta sẽ thêm một số mã vào các tệp proto của mình.
proto/auth.proto
syntax = "proto3";
package auth;
service AuthService {
rpc Register (RegisterRequest) returns (RegisterResponse) {}
rpc Login (LoginRequest) returns (LoginResponse) {}
rpc Validate (ValidateRequest) returns (ValidateResponse) {}
}
// Register
message RegisterRequest {
string email = 1;
string password = 2;
}
message RegisterResponse {
int32 status = 1;
repeated string error = 2;
}
// Login
message LoginRequest {
string email = 1;
string password = 2;
}
message LoginResponse {
int32 status = 1;
repeated string error = 2;
string token = 3;
}
// Validate
message ValidateRequest {
string token = 1;
}
message ValidateResponse {
int32 status = 1;
repeated string error = 2;
int32 userId = 3;
}
proto/order.proto
syntax = "proto3";
package order;
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse) {}
}
message CreateOrderRequest {
int32 productId = 1;
int32 quantity = 2;
int32 userId = 3;
}
message CreateOrderResponse {
int32 status = 1;
repeated string error = 2;
int32 id = 3;
}
proto/product.proto
syntax = "proto3";
package product;
service ProductService {
rpc CreateProduct (CreateProductRequest) returns (CreateProductResponse) {}
rpc FindOne (FindOneRequest) returns (FindOneResponse) {}
rpc DecreaseStock (DecreaseStockRequest) returns (DecreaseStockResponse) {}
}
// CreateProduct
message CreateProductRequest {
string name = 1;
string sku = 2;
int32 stock = 3;
double price = 4;
}
message CreateProductResponse {
int32 status = 1;
repeated string error = 2;
int32 id = 3;
}
// FindOne
message FindOneData {
int32 id = 1;
string name = 2;
string sku = 3;
int32 stock = 4;
double price = 5;
}
message FindOneRequest {
int32 id = 1;
}
message FindOneResponse {
int32 status = 1;
repeated string error = 2;
FindOneData data = 3;
}
// DecreaseStock
message DecreaseStockRequest {
int32 id = 1;
}
message DecreaseStockResponse {
int32 status = 1;
repeated string error = 2;
}
Sau đó bạn cần public project lên github
API GATEWAY
Trong project này, chúng tôi sẽ chuyển tiếp các yêu cầu HTTP tới microservice của mình.
Cài đặt
npm i @nestjs/microservices @grpc/grpc-js @grpc/proto-loader
npm i -D @types/node ts-proto
Cấu trúc dự án
nest g mo auth && nest g co auth --no-spec && nest g s auth --no-spec
nest g mo product && nest g co product --no-spec
nest g mo order && nest g co order --no-spec
touch src/auth/auth.guard.ts
Scripts
Thêm 5 script này vào package.json, thay {{link_git_proto}} bằng link git proto bạn đã public bên trên
"proto:install": "npm i {{link_git_proto}}",
"proto:auth": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./node_modules/grpc-nest-proto/proto --ts_proto_out=src/auth/ node_modules/grpc-nest-proto/proto/auth.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
"proto:order": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./node_modules/grpc-nest-proto/proto --ts_proto_out=src/order/ node_modules/grpc-nest-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=./node_modules/grpc-nest-proto/proto --ts_proto_out=src/product/ node_modules/grpc-nest-proto/proto/product.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
"proto:all": "npm run proto:auth && npm run proto:order && npm run proto:product"
chạy script:
npm run proto:install && npm run proto:all
proto:install
sẽ cài đặt kho lưu trữ proto được chia sẻ dưới dạng gói NPMproto:all
sẽ tạo các tệp protobuf với hậu tố bên.pb.ts
trong các mô-đun:auth
,order
vàproduct
dự án sẽ trông như thế này:
Auth
Auth module
import { Global, Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AuthController } from './auth.controller';
import { AUTH_SERVICE_NAME, AUTH_PACKAGE_NAME } from './auth.pb';
import { AuthService } from './auth.service';
@Global()
@Module({
imports: [
ClientsModule.register([
{
name: AUTH_SERVICE_NAME,
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50051',
package: AUTH_PACKAGE_NAME,
protoPath: 'node_modules/grpc-nest-proto/proto/auth.proto',
},
},
]),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
Auth Service
import { Inject, Injectable } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
import { AuthServiceClient, AUTH_SERVICE_NAME, ValidateResponse } from './auth.pb';
@Injectable()
export class AuthService {
private svc: AuthServiceClient;
@Inject(AUTH_SERVICE_NAME)
private readonly client: ClientGrpc;
public onModuleInit(): void {
this.svc = this.client.getService<AuthServiceClient>(AUTH_SERVICE_NAME);
}
public async validate(token: string): Promise<ValidateResponse> {
return firstValueFrom(this.svc.validate({ token }));
}
}
Auth Guard
import { Injectable, CanActivate, ExecutionContext, HttpStatus, UnauthorizedException, Inject } from '@nestjs/common';
import { Request } from 'express';
import { ValidateResponse } from './auth.pb';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
@Inject(AuthService)
public readonly service: AuthService;
public async canActivate(ctx: ExecutionContext): Promise<boolean> | never {
const req: Request = ctx.switchToHttp().getRequest();
const authorization: string = req.headers['authorization'];
if (!authorization) {
throw new UnauthorizedException();
}
const bearer: string[] = authorization.split(' ');
if (!bearer || bearer.length < 2) {
throw new UnauthorizedException();
}
const token: string = bearer[1];
const { status, userId }: ValidateResponse = await this.service.validate(token);
req.user = userId;
if (status !== HttpStatus.OK) {
throw new UnauthorizedException();
}
return true;
}
}
Auth Controller
import { Body, Controller, Inject, OnModuleInit, Post, Put } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import { AuthServiceClient, RegisterResponse, RegisterRequest, AUTH_SERVICE_NAME, LoginRequest, LoginResponse } from './auth.pb';
@Controller('auth')
export class AuthController implements OnModuleInit {
private svc: AuthServiceClient;
@Inject(AUTH_SERVICE_NAME)
private readonly client: ClientGrpc;
public onModuleInit(): void {
this.svc = this.client.getService<AuthServiceClient>(AUTH_SERVICE_NAME);
}
@Post('register')
private async register(@Body() body: RegisterRequest): Promise<Observable<RegisterResponse>> {
return this.svc.register(body);
}
@Put('login')
private async login(@Body() body: LoginRequest): Promise<Observable<LoginResponse>> {
return this.svc.login(body);
}
}
Order
Order module
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ORDER_SERVICE_NAME, ORDER_PACKAGE_NAME } from './order.pb';
import { OrderController } from './order.controller';
@Module({
imports: [
ClientsModule.register([
{
name: ORDER_SERVICE_NAME,
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50052',
package: ORDER_PACKAGE_NAME,
protoPath: 'node_modules/grpc-nest-proto/proto/order.proto',
},
},
]),
],
controllers: [OrderController],
})
export class OrderModule {}
Order controller
import { Controller, Inject, Post, OnModuleInit, UseGuards, Req } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import { CreateOrderResponse, OrderServiceClient, ORDER_SERVICE_NAME, CreateOrderRequest } from './order.pb';
import { AuthGuard } from '../auth/auth.guard';
import { Request } from 'express';
@Controller('order')
export class OrderController implements OnModuleInit {
private svc: OrderServiceClient;
@Inject(ORDER_SERVICE_NAME)
private readonly client: ClientGrpc;
public onModuleInit(): void {
this.svc = this.client.getService<OrderServiceClient>(ORDER_SERVICE_NAME);
}
@Post()
@UseGuards(AuthGuard)
private async createOrder(@Req() req: Request): Promise<Observable<CreateOrderResponse>> {
const body: CreateOrderRequest = req.body;
body.userId = <number>req.user;
return this.svc.createOrder(body);
}
}
Product
Product Module
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { PRODUCT_PACKAGE_NAME, PRODUCT_SERVICE_NAME } from './product.pb';
import { ProductController } from './product.controller';
@Module({
imports: [
ClientsModule.register([
{
name: PRODUCT_SERVICE_NAME,
transport: Transport.GRPC,
options: {
url: '0.0.0.0:50053',
package: PRODUCT_PACKAGE_NAME,
protoPath: 'node_modules/grpc-nest-proto/proto/product.proto',
},
},
]),
],
controllers: [ProductController],
})
export class ProductModule {}
Product Controller
import { Controller, Get, Inject, OnModuleInit, Param, ParseIntPipe, UseGuards, Post, Body } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import {
FindOneResponse,
ProductServiceClient,
PRODUCT_SERVICE_NAME,
CreateProductResponse,
CreateProductRequest,
} from './product.pb';
import { AuthGuard } from '../auth/auth.guard';
@Controller('product')
export class ProductController implements OnModuleInit {
private svc: ProductServiceClient;
@Inject(PRODUCT_SERVICE_NAME)
private readonly client: ClientGrpc;
public onModuleInit(): void {
this.svc = this.client.getService<ProductServiceClient>(PRODUCT_SERVICE_NAME);
}
@Post()
@UseGuards(AuthGuard)
private async createProduct(@Body() body: CreateProductRequest): Promise<Observable<CreateProductResponse>> {
return this.svc.createProduct(body);
}
@Get(':id')
@UseGuards(AuthGuard)
private async findOne(@Param('id', ParseIntPipe) id: number): Promise<Observable<FindOneResponse>> {
return this.svc.findOne({ id });
}
}
API Gateway đã được hoàn thành. Bây giờ chúng ta có thể chạy nó.
All rights reserved