用代码实现了一个基于 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> 存储数据。创建用户时会做几件事:
- 检查 email 是否重复
- 检查 phone 是否重复
- 使用 bcrypt 加密密码
- 使用 nanoid 生成用户 ID
- 记录 created_at 和 updated_at
- 返回用户信息时去掉 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 方法能否调用成功,至少要同时满足三点:
- proto 中声明了 service 和 rpc
- controller 中使用 @GrpcMethod() 绑定了方法
- 对应 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 示例,逐步演进成更接近生产环境的微服务基础工程。