跳转到正文
YGJoon 技术博客

NestJS 后端入门:Controller、Provider、Module 与依赖注入

发布于:
·1.3k 字 · 约 3 分钟·作者:YGJoon
Controller、Provider、Module 三件套各管什么?依赖注入为什么比 new 好用?DTO 配 ValidationPipe 做参数校验。
NestJS 后端入门:Controller、Provider、Module 与依赖注入文章封面
目录

NestJS 是 Node.js 生态里最”像 Spring”的框架:装饰器、依赖注入、模块化,开箱即用。它的核心就三件套——Controller(接请求)、Provider(写逻辑)、Module(组装)——理解了这三个,Nest 就入门了。

Controller:只负责接请求

Controller 是 HTTP 的入口,只做三件事:收参数、调服务、返结果。不要在 Controller 里写业务逻辑:

// users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users') // 路由前缀 /users
export class UsersController {
  // 依赖通过构造函数注入,Nest 自动 new 好传进来
  constructor(private readonly usersService: UsersService) {}

  @Get() // GET /users
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id') // GET /users/123
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  @Post() // POST /users
  create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }
}

装饰器即路由:@Get(':id') + @Param('id') 取路径参数,@Body() 取请求体,@Query() 取查询参数。返回值直接被序列化成 JSON,不用手动 res.json()。

Provider:真正的业务逻辑在这里

Service 是最常见的 Provider,本质就是一个被 @Injectable() 标记的类:

// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

interface User {
  id: string;
  name: string;
  email: string;
}

@Injectable()
export class UsersService {
  private users: User[] = [];

  findAll(): User[] {
    return this.users;
  }

  findOne(id: string): User {
    const user = this.users.find((u) => u.id === id);
    if (!user) {
      // 抛出的 HttpException 会被转成对应状态码的 JSON
      throw new NotFoundException(`用户 ${id} 不存在`);
    }
    return user;
  }

  create(dto: CreateUserDto): User {
    const user = { id: Date.now().toString(), ...dto };
    this.users.push(user);
    return user;
  }
}

依赖注入:为什么不用 new?

上面的 Controller 里 constructor(private readonly usersService: UsersService),我们从来没 new UsersService(),是 Nest 的 IoC 容器自动创建并注入的。好处:

  1. 解耦:Controller 不关心 Service 怎么构造(需不需要传数据库连接?需不需要配置文件?)
  2. 单例:默认同一个 Provider 在整个应用是单例,不用自己维护
  3. 可测试:单元测试时可以轻松 mock 掉 Service
// 单元测试:直接 mock,不用起数据库
const module = await Test.createTestingModule({
  controllers: [UsersController],
  providers: [
    {
      provide: UsersService,
      useValue: { findAll: () => [{ id: '1', name: 'mock' }] },
    },
  ],
}).compile();

想注入别的东西(比如配置、数据库连接),用 @Inject() + token:

@Injectable()
export class UsersService {
  constructor(@Inject('DB') private readonly db: any) {}
}

Module:把三件套组装起来

Module 是组织单元,一个业务域一个 Module:

// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController], // 这个模块的入口
  providers: [UsersService],      // 这个模块的零件
  exports: [UsersService],        // 允许别的模块用(不写就只能本模块用)
})
export class UsersModule {}

根模块把所有业务模块拼起来:

// app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule],
})
export class AppModule {}

A 模块想用 B 模块的 Service:B 在 exports 里导出,A 在 imports 里引入 B 的 Module。规则就这么简单。

DTO + ValidationPipe:参数校验一次配好

DTO(Data Transfer Object)定义接口收什么参数,配合 class-validator 自动校验:

// dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  name: string;

  @IsEmail({}, { message: '邮箱格式不正确' })
  email: string;

  @MinLength(6, { message: '密码至少 6 位' })
  password: string;
}

在 main.ts 全局启用,之后所有 @Body() 都会自动校验,不通过直接返回 400:

// main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 自动剥掉 DTO 里没定义的字段
      transform: true,  // 自动把 plain object 转成 DTO 实例
    }),
  );
  await app.listen(3000);
}
bootstrap();

whitelist: true 很实用:前端多传了 isAdmin: true 这种字段会被直接丢掉,进不到业务逻辑。

实战案例

来自个人全栈项目的一个后台接口:用户管理 CRUD,本来是 Express 手写,迁移到 NestJS。

迁移时第一个决策是要不要把原来的工具函数改成 Provider。原来的 utils/db.js 是直接 export 函数,处处 require。我把它包成 @Injectable() 的 DatabaseService,好处是单元测试时可以用 useValue mock 掉,不用连真实数据库。但代价是所有调用方都要改成构造函数注入,改了 20 多个文件。结论:新项目直接上 Provider 毫无疑问;老项目迁移,评估下改动量再动手。

第二个坑是 DTO 的 transform: true。有个接口收 page 查询参数,前端传的是字符串 "2",DTO 里写 page: number,没开 transform 时 @IsInt() 校验直接挂(字符串不是整数)。开了 transform: true 后 Nest 会按 DTO 的类型元数据自动转,"2" 变成 2。但注意这依赖 reflect-metadata 和装饰器发出的类型信息,@Query() 的 DTO 同样生效。

现在的习惯:所有入参必写 DTO,所有 DTO 字段必加校验装饰器,Controller 里不出现任何 if (!body.name) 这种手写校验。三个月下来,参数相关的 400 错误全是前端传错,定位时间从小时级降到分钟级。


分享这篇文章:


上一篇
Nuxt 3 服务端渲染实践:路由、数据获取与水合避坑