深入浅出 NestJS 之服务 (Services)
在 NestJS 的分层架构中,Service(服务) 作为核心组件之一,承担着至关重要的角色。很多初学者可能会疑惑:Service 到底是用来做什么的?它和 Controller 有什么区别?今天我们就来深入剖析 NestJS 中 Service 的作用,结合实例带你掌握其使用精髓。
先搞懂:为什么需要 Service?
先思考一个问题:如果没有 Service,NestJS 项目会变成什么样? 假设我们直接在 Controller(控制器)中编写所有业务逻辑,比如数据查询、计算、第三方接口调用等。这样做会导致两个严重问题:
- Controller 职责过重:Controller 的核心职责本是 “接收请求、返回响应”,若混入大量业务逻辑,会让代码变得臃肿不堪,难以维护。
- 代码无法复用:当多个 Controller 需要用到相同的业务逻辑(比如用户权限校验、数据格式化)时,只能重复编写代码,违背 “DRY(Don't Repeat Yourself)” 原则。
而 Service 的出现,正是为了解决这些问题。它就像一个 “业务逻辑处理器”,专门负责封装复杂业务,让 Controller 回归本职,同时实现代码的复用与解耦。
Service 的核心作用:3 个维度
在 NestJS 中,Service 的作用可以概括为 “封装业务、解耦职责、支持复用”,具体可从以下 3 个维度展开:
1. 封装业务逻辑:让 Controller 轻装上阵
Service 的首要作用是承载所有业务逻辑,包括数据处理、计算、规则校验、第三方服务调用等。Controller 只需要 “调用 Service 的方法”,无需关心业务逻辑的具体实现。
2. 实现代码复用:一处编写,多处调用
Service 的另一个核心作用是复用。当多个 Controller 或其他 Service 需要用到相同的业务逻辑时,只需注入对应的 Service 并调用方法,无需重复编写代码。
3. 支持依赖注入:解耦组件依赖
NestJS 基于 依赖注入(Dependency Injection, DI) 设计,而 Service 是依赖注入的核心载体。通过 @Injectable() 装饰器标记 Service,NestJS 会自动管理 Service 的实例,并在需要时注入到 Controller 或其他 Service 中。 这种机制带来两个好处:
- 解耦依赖:Controller 不需要手动创建 Service 实例,只需声明 “需要哪个 Service”,由 NestJS 负责注入,降低了组件间的耦合度。
- 便于测试:在单元测试中,可以轻松替换 Service 的实现(比如用模拟数据的 Service 替代真实数据库操作的 Service),无需修改 Controller 代码。
依赖注入的底层逻辑:
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
// 如上的代码类似于如下代码
// private readonly userService: UserService;
// constructor(userService: UserService) {
// this.userService = userService;
// }
@Get()
findAll(): User[] {
return this.userService.findAll();
}
}
当我们在 Controller 的构造函数中声明 private userService: UserService 时,NestJS 会:
- 扫描装饰器: 检查 UserService 是否被
@Injectable()标记(确保可注入); - 分析依赖:分析构造函数的参数,确定需要注入的依赖
- 创建单例:创建 UserService 的单例实例(默认是单例,可配置作用域);
- 注入依赖:将实例注入到 UserController 中,供其调用,可以使用 this.userService 调用 Service 的方法。
PS:依赖注入(Dependency Injection,简称 DI)是一种设计模式,它让类不需要自己创建依赖对象,而是由外部(通常是框架)提供
Service 的最佳实践:避免踩坑
需遵循单一职责
一个 Service 只负责一个领域的业务,不要把所有业务逻辑都塞进一个 Service(比如创建一个 CommonService 处理所有通用逻辑)。正确的做法是:按业务领域拆分 Service,比如 UserService(用户相关)、OrderService(订单相关)、AuthService(权限相关),每个 Service 只处理自己领域内的逻辑。
这样做的好处是:代码结构清晰,便于定位问题,也利于团队协作(不同开发者负责不同领域的 Service)。
不要在 Service 中处理 HTTP 相关逻辑
Service 是 “业务逻辑层”,不应该依赖 HTTP 相关的对象(比如 Request、Response),也不应该直接返回 HTTP 状态码或响应格式。这些操作应该由 Controller 负责。
错误(Service 处理 HTTP 逻辑)❌ ❌
// 错误:Service 直接返回 HTTP 响应
@Injectable()
export class UserService {
async getUserById(id: string, res: Response) { // 依赖 Response 对象
const user = await this.prisma.user.findUnique({ where: { id: Number(id) } });
if (!user) {
res.status(404).send('用户不存在'); // 直接操作响应
}
res.send(user);
}
}
正确(Service 处理 HTTP 逻辑) ✅ ✅
// 正确:Service 返回数据或抛出异常
@Injectable()
export class UserService {
async getUserById(id: string) {
const user = await this.userRepo.findUnique({ where: { id: Number(id) } });
if (!user) {
throw new NotFoundException('用户不存在'); // 抛出异常
}
return user; // 返回数据
}
}
// Controller 处理 HTTP 响应
@Controller('users')
export class UserController {
@Get(':id')
async getUserById(@Param('id') id: string) {
return this.userService.getUserById(id); // NestJS 自动处理响应状态码
}
}
合理使用 Service 之间的依赖注入
当一个 Service 需要调用另一个 Service 的逻辑时,可以直接在构造函数中注入对方的 Service(注意避免循环依赖)。
示例:OrderService 依赖 UserService
@Injectable()
export class OrderService {
// 注入 UserService
constructor(private userService: UserService, private prisma: PrismaService) {}
async createOrder(userId: string, productId: string) {
// 1. 先通过 UserService 校验用户是否存在
await this.userService.getUserById(userId);
// 2. 创建订单, orderRepo 通常和 数据打交道
return this.orderRepo.create({
userId: Number(userId),
productId: Number(productId),
status: 'pending'
});
}
}
NestJS 中创建 Service
1. nest g service name 创建 service 类
nest g service user // user 是服务的名字
这个命令会:
- 创建 src/user/user.service.ts 文件
- 创建 src/user/user.service.spec.ts 测试文件
- 如果 user.module.ts 存在,会自动在模块中注册服务
2. 编辑 src/user/user.service.ts 文件
import { Injectable } from '@nestjs/common';
@Injectable() // 是能被依赖注入的标识
export class UserService {
constructor(private readonly orderService: OrderService,private readonly userRepo: UserRepo) {}
async findOne(id) {
// 创建用户逻辑
const user = await this.userRepo.find(userData);
// 可以调用其他服务的方法
const order = await this.orderService.findOrderByUserId(id);
return {
...user,
order,
};
}
}
@Injectable() 装饰器 的作用
- NestJS 的依赖注入系统通过
@Injectable()装饰器标记的类才能被注入 - 元数据生成:装饰器会生成元数据,帮助 NestJS 了解类的依赖关系
3. 编辑 src/user/user.controller.ts 文件
在 src/user/user.controller.ts 注入 服务
import { Injectable } from '@nestjs/common';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number): User {
const user = this.userService.findOne(id);
if (!user) {
throw new Error(`User with id ${id} not found`);
}
return user;
}
}
4. 编辑 src/user/user.module.ts 和 src/app.module.ts 文件
// src/user/user.module.ts
@Module({
imports: [],
controllers: [UserController],
providers: [UserService],
exports: [],
})
export class UserModule {}
// src/app.module.ts
@Module({
// imports 用来导入其他模块
imports: [UserModule],
// controllers 用来注册控制器,控制器负责处理 HTTP 请求
controllers: [AppController, UserController],
// providers 用来注册提供者,通常是服务类,包含业务的逻辑
providers: [AppService, UserService],
})
export class AppModule {}
总结:Service 是 NestJS 架构的 “业务核心”
如果把 NestJS 项目比作一家公司:
- Controller 像是 “前台”,负责接待客户(接收请求)、传递需求(调用 Service)、反馈结果(返回响应);
- Service 像是 “业务部门”,负责处理核心业务(封装逻辑)、跨部门协作(复用逻辑)、支撑公司运转(依赖注入)。 理解 Service 的作用,不仅能让你写出更清晰、更可维护的代码,更能帮助你掌握 NestJS 分层架构的设计思想。在实际开发中,记得始终遵循 “Controller 负责请求响应,Service 负责业务逻辑” 的原则,让你的项目结构更优雅、扩展性更强。