本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。
1. 这节课要实现的功能是什么
- 创建一个可运行的 NestJS BBS 项目。
- 搭建
UsersModule,提供最小可用接口:POST /users、GET /users、GET /users/:id。 - 建立你对 Nest 核心分层的第一套心智模型:
Module -> Controller -> Service。
2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)
本节先把概念讲清楚,再进入代码。
2.1 @Module() 是什么
@Module() 是 Nest 的模块声明器。你可以把它理解成一个“功能容器”:
imports:这个容器依赖了哪些其他容器。controllers:这个容器对外暴露哪些 HTTP 入口。providers:这个容器内部可注入的服务(业务、仓储、工具等)。exports:这个容器愿意对外共享哪些 Provider。
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
2.2 imports 和 providers 到底有什么区别
这是初学 Nest 最容易混淆的一组概念。你可以先记住一句话:
providers:这个模块自己创建、自己管理的依赖;imports:这个模块要使用的“其他模块已经提供好的能力”。
再补上 exports,三者关系就完整了:
providers:我自己生产;imports:我使用别人生产的;exports:我允许别人使用我生产的。
举个最小例子:
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
这里表示:
UsersModule自己负责创建UsersService;- 并且愿意把
UsersService对外导出。
如果另一个模块想复用这份 UsersService,正确做法不是把 UsersService 再写到自己的 providers 里,而是导入 UsersModule:
@Module({
imports: [UsersModule],
})
export class PostsModule {}
为什么不建议在多个模块里重复写同一个类到 providers?
因为 providers 的语义是“本模块自己提供”,不同模块分别声明时,通常会得到不同实例。只有通过 imports + exports 复用时,才是在使用同一份模块能力。
2.3 @Controller() 是什么
@Controller() 负责接收 HTTP 请求并返回响应。它是“接口层”,不应该承担复杂业务逻辑。
2.4 @Injectable() 是什么
@Injectable() 表示这个类可被 Nest 的 DI 容器管理和注入。通常 Service 都会加这个装饰器。
2.5 依赖注入(DI)是什么
DI 的核心是“我不手动 new 依赖,而是让框架注入”。
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
}
这句构造函数注入,让 Controller 依赖 Service,但不关心 Service 如何创建,代码更易测试、替换和扩展。
2.6 不创建 Controller 可以使用模块吗
可以。很多基础设施模块只有 providers,没有 controllers,依然完全可用。
import { Global, Module } from '@nestjs/common';
import { DatabaseService } from './database.service';
@Global()
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}
2.7 必须 AppModule 直接导入自定义模块吗
不必须“直接导入”,但必须在根模块的导入链上。也就是说,只要能通过 imports 链路连到 AppModule,模块就会生效。
// app.module.ts
@Module({ imports: [ForumModule] })
export class AppModule {}
// forum.module.ts
@Module({ imports: [UsersModule] })
export class ForumModule {}
2.8 会有多个 Module 依赖的场景吗,怎么处理
会,而且很常见。处理方式是:
- 依赖方在
imports声明被依赖模块。 - 被依赖模块在
exports暴露必要 Provider。 - 不跨模块访问未导出的内部实现。
- 循环依赖短期可用
forwardRef,长期应该重构模块边界。
// users.module.ts
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// posts.module.ts
@Module({
imports: [UsersModule],
providers: [PostsService],
})
export class PostsModule {}
2.9 为什么 providers 要写 token,exports 到底该导出什么
很多同学在这一步会疑惑:既然已经有 UsersService,为什么还要写这种配置?
@Module({
providers: [
UsersService,
{ provide: USERS_REPOSITORY, useClass: InMemoryUsersRepository },
],
exports: [UsersService, USERS_REPOSITORY],
})
核心原因不是“写法更高级”,而是“依赖解耦”。
UsersService代表业务能力;USERS_REPOSITORY(Symbol token)代表存储抽象;InMemoryUsersRepository只是当前实现。
这样写以后,UsersService 依赖的是“抽象 token”,不是某个具体类。后续你把内存仓储替换成 Prisma 仓储,通常只需要改模块里的绑定关系,不需要大改业务层代码。
再看 exports:
- 导出
UsersService:其他模块可以直接注入UsersService; - 导出
USERS_REPOSITORY:其他模块可以通过@Inject(USERS_REPOSITORY)注入仓储抽象; - 只导出 token 也可以,但那样其他模块拿不到
UsersService。
下面是两种注入方式的对比:
// 注入类 provider(不需要 @Inject)
constructor(private readonly usersService: UsersService) {}
// 注入 token provider(必须 @Inject)
constructor(
@Inject(USERS_REPOSITORY)
private readonly usersRepository: UsersRepository,
) {}
所以,判断 exports 里该放什么的原则很简单:你希望别的模块能注入什么,就导出什么。
3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)
现在开始把上面的概念落地到本课代码。
3.1 src/users/users.service.ts
先写 Service,让业务逻辑有归属。Service 内部维护一个内存数组,模拟数据存储。
import { Injectable } from '@nestjs/common';
export interface User {
id: number;
username: string;
email: string;
}
@Injectable()
export class UsersService {
private readonly users: User[] = [];
private nextId = 1;
create(username: string, email: string): User {
const user: User = {
id: this.nextId++,
username,
email,
};
this.users.push(user);
return user;
}
findAll(): User[] {
return [...this.users];
}
}
3.2 src/users/users.controller.ts
再写 Controller,把 HTTP 路由映射到 Service 方法。
import { Body, Controller, Get, Post } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Post()
create(@Body() body: { username: string; email: string }) {
return this.usersService.create(body.username, body.email);
}
}
3.3 src/users/users.module.ts
然后用 UsersModule 把这个功能域组装起来。
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 {}
3.4 src/app.module.ts
最后在根模块挂载 UsersModule,让整个应用能够识别这个功能域。
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
})
export class AppModule {}
3.5 请求是如何流转的
当你调用 POST /users 时,执行路径是:
- 请求进入
UsersController.create。 - Controller 调用
UsersService.create。 - Service 处理业务并返回结果。
- Controller 把结果作为 HTTP 响应返回。
这个分层看起来简单,但它是后续所有高级能力(鉴权、日志、事务、缓存、测试)的基础。
3.6 本课验证命令
npm run start:dev
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"username":"tom","email":"tom@example.com","password":"password123"}'
curl http://localhost:3000/users
4. 总结这节课知识点
- 你先建立了
Module / Controller / Service / DI的完整概念,再把它们落地到代码。 - 你理解了
@Module在 Nest 中的真实职责:边界、依赖、暴露能力。 - 你知道了模块不一定要有 Controller,也知道了模块不必被
AppModule直接导入。 - 你掌握了多模块依赖的标准处理方式:
imports + exports。
5. 每节课分支
lesson-01-foundation- 远程仓库:
https://github.com/Shiuwn/nestjs-tutorial.git