用 NestJS 构建一个同时支持用户与订单的 gRPC 微服务

用代码实现了一个基于 NestJS 的 gRPC 微服务。虽然项目名叫 user,但它实际上同时承载了两个业务域:用户服务和订单服务。整个项目围绕 proto 文件定义接口协议,再通过 NestJS 的模块、控制器和服务完成具体业务逻辑。

一、项目整体结构

项目的核心代码主要分为三部分:

css 复制代码
src/main.ts 
src/app.module.ts 
src/user/ 
    user.controller.ts 
    user.service.ts 
    user.module.ts 
  src/order/ 
      order.controller.ts 
      order.service.ts 
      order.module.ts 
  proto/ 
      user.proto 
      order.proto

其中 proto/user.proto 定义用户相关的 gRPC 服务,proto/order.proto 定义订单相关的 gRPC 服务。NestJS 通过 @GrpcMethod() 把 TypeScript 方法绑定到 proto 中声明的 RPC 方法上。

二、gRPC 服务启动入口

项目入口是 src/main.ts。

它使用:

scss 复制代码
NestFactory.createMicroservice<MicroserviceOptions>() 

创建一个纯 gRPC 微服务,而不是普通 HTTP 应用。

关键配置如下:

css 复制代码
transport: Transport.GRPC, 
options: { 
    url: '0.0.0.0:50052', 
    package: ['user', 'order'], 
    protoPath: [ 
        join(__dirname, '../proto/user.proto'), 
        join(__dirname, '../proto/order.proto') 
    ], 
}

这里有几个重点:

第一,服务监听在 50052 端口。

第二,package 配置了 'user', 'order',表示这个 gRPC 服务会同时注册 user.UserService 和 order.OrderService。如果这里只写 'user',即使加载了 order.proto,订单服务也不会真正绑定,调用时就会出现 Unimplemented。

第三,项目引入了 @grpc/reflection,通过反射可以用 grpcurl list 查看当前服务暴露了哪些 gRPC 方法,这对于调试非常有帮助。

三、用户服务设计

用户协议定义在 proto/user.proto 中。

它声明了一个 UserService:

scss 复制代码
service UserService { 
    rpc CreateUser(CreateUserRequest) returns (User); 
    rpc GetUser(GetUserRequest) returns (User); 
    rpc UpdateUser(UpdateUserRequest) returns (User); 
    rpc DeleteUser(DeleteUserRequest) returns (EmptyResponse); 
    rpc ListUsers(ListUsersRequest) returns (UserListResponse); 
}

对应的 NestJS 控制器是 src/user/user.controller.ts。

例如创建用户:

kotlin 复制代码
@GrpcMethod('UserService', 'CreateUser') 
createUser(createUserDto: CreateUserDto) { return this.userService.create(createUserDto); }

这里的 'UserService' 和 'CreateUser' 必须和 proto 文件里的服务名、方法名对应。gRPC 对方法路径非常敏感,最终客户端调用的是:

复制代码
user.UserService/CreateUser

真正的业务逻辑在 UserService 中。

用户服务目前使用内存 Map<string, User> 存储数据。创建用户时会做几件事:

  1. 检查 email 是否重复
  2. 检查 phone 是否重复
  3. 使用 bcrypt 加密密码
  4. 使用 nanoid 生成用户 ID
  5. 记录 created_at 和 updated_at
  6. 返回用户信息时去掉 password

这一点很重要:内部 User 对象包含密码,但对外返回的是 UserResponse,避免把密码泄露给客户端。

四、gRPC 错误处理

用户服务里没有直接使用 HTTP 异常,而是使用了:

javascript 复制代码
import { status } from '@grpc/grpc-js';
import { RpcException } from '@nestjs/microservices';

RpcException

并配合:

lua 复制代码
status.ALREADY_EXISTS status.NOT_FOUND status.INVALID_ARGUMENT

例如 email 重复时:

arduino 复制代码
throw new RpcException({ code: status.ALREADY_EXISTS, message: 'Email already exists', });

这样 grpcurl 调用时可以看到清晰的错误:

arduino 复制代码
Code: AlreadyExists Message: Email already exists

如果在 gRPC 服务里直接抛 ConflictException、NotFoundException 这类 HTTP 异常,客户端往往只能看到:

vbnet 复制代码
Code: Unknown Message: Internal server error

所以在 gRPC 项目中,业务异常最好显式转换成 gRPC 状态码。

五、订单服务设计

订单协议定义在 proto/order.proto 中。

它声明了 OrderService:

scss 复制代码
service OrderService { 
    rpc CreateOrder(CreateOrderRequest) returns (Order); 
    rpc GetOrder(GetOrderRequest) returns (Order); 
    rpc GetUserOrders(GetUserOrdersRequest) returns (OrderListResponse); 
    rpc UpdateOrderStatus(UpdateOrderStatusRequest) returns (Order); 
    rpc DeleteOrder(DeleteOrderRequest) returns (EmptyResponse); 
}

对应控制器是 src/order/order.controller.ts。

例如创建订单:

kotlin 复制代码
@GrpcMethod('OrderService', 'CreateOrder') 
createOrder(createOrderDto: CreateOrderDto) { return this.orderService.create(createOrderDto); }

客户端调用路径是:

css 复制代码
order.OrderService/CreateOrder

订单服务同样使用内存 Map 存储订单。创建订单时会生成订单 ID,默认状态是:

复制代码
OrderStatus.PENDING

订单支持按 ID 查询、按用户 ID 查询、更新订单状态以及删除订单。

六、模块注册是 gRPC 能否调用成功的关键

项目中 AppModule 引入了两个业务模块:

ini 复制代码
imports: [UserModule, OrderModule]

这一步很关键。

即使你写好了 UserController 或 OrderController,如果对应模块没有被 AppModule 导入,NestJS 启动时也不会注册这些 gRPC handler。结果就是 grpcurl 调用时出现:

css 复制代码
    Code: Unimplemented Message: The server does not implement the method

所以 gRPC 方法能否调用成功,至少要同时满足三点:

  1. proto 中声明了 service 和 rpc
  2. controller 中使用 @GrpcMethod() 绑定了方法
  3. 对应 module 被 AppModule imports 进来

七、TypeScript 类型与 proto 的对应关系

项目里还有 user.interface.ts 和 order.interface.ts,用于在 TypeScript 侧描述请求和响应结构。

比如订单相关类型:

typescript 复制代码
export interface GetOrderRequest { id: string; } 
export interface GetUserOrdersRequest { user_id: string; } 
export interface DeleteOrderRequest { id: string; }

这些类型和 proto 中的 message 保持一致,可以让控制器和服务拥有更好的类型提示。

同时,项目中也定义了 gRPC 客户端接口:

scss 复制代码
export interface OrderServiceClient { 
    createOrder(request: CreateOrderDto): Observable<Order>; 
    getOrder(request: GetOrderRequest): Observable<Order>; 
    getUserOrders(request: GetUserOrdersRequest): 
    Observable<OrderListResponse>; 
}

这类接口适合在网关层或其他服务中使用 ClientGrpc 调用远程 gRPC 服务。

八、项目中的几个实践点

这个项目体现了几个很实用的 NestJS gRPC 开发经验。

第一,proto 是契约中心。服务端、客户端、grpcurl 调用都要以 proto 中的 package、service、rpc 名称为准。

第二,多个 proto package 可以由同一个 NestJS gRPC 服务承载,但 package 必须配置成数组:

package: ['user', 'order']

第三,gRPC 服务里应该使用 RpcException 返回明确的 gRPC 状态码,而不是直接抛 HTTP 异常。

第四,nanoid@5 是 ESM-only,在当前 CommonJS 输出环境下需要通过动态 import 使用:

const { nanoid } = await import('nanoid');

第五,start:prod 要指向真实构建产物:

"start:prod": "node dist/main.js"

九、总结

这个项目是一个典型的 NestJS gRPC 微服务示例:用 proto 定义服务契约,用 NestJS 模块组织业务,用 @GrpcMethod() 暴露 RPC 方法,用内存 Map 模拟数据存储。

用户服务负责用户创建、查询、更新、删除和分页列表,并处理密码加密与重复校验;订单服务负责订单创建、查询、按用户查询、状态更新和删除。两个服务共同运行在 50052 端口上,通过 user.UserService 和 order.OrderService 对外提供 gRPC 能力。

如果后续要继续完善,这个项目下一步可以把内存 Map 替换成数据库存储,再加入参数校验、认证鉴权、日志追踪和更完整的测试。这样它就可以从一个教学型 gRPC 示例,逐步演进成更接近生产环境的微服务基础工程。

相关推荐
DyLatte1 小时前
很多人把坚持,误以为成长
前端·后端·程序员
小马爱打代码1 小时前
SpringBoot + 延迟消息 + 时间轮:订单超时、优惠券过期等场景的高效实现方案
java·spring boot·后端
长大19881 小时前
MySQL 索引失效常见场景:开发优化必记要点
后端
达达尼昂1 小时前
AI Native 工程实践 : agent 自动化测试
前端·后端·架构
爱勇宝2 小时前
写给年轻程序员:别急着证明自己,也别太早放过自己
前端·后端·程序员
kungggyoyoyo2 小时前
从0开发一套geo优化软件:数据模型与API设计
前端·vue.js·后端
用户34232323763172 小时前
数据模型与地址映射——为什么你读到的永远是错位的数据
后端
To_OC2 小时前
我调用 DeepSeek API 连踩 3 个坑,终于把 Node AIGC 开发的核心知识点捋顺了
后端·node.js·aigc
在下赵铁柱2 小时前
Spring Boot 防重复提交:从按钮连点到重复下单,一个 AOP 注解真的够吗?
后端