白留白WHITE SPACE
返回所有文章
NestJS / 后端开发

NestJS 实战 01:项目初始化与模块化分层

创建一个可运行的 NestJS BBS 项目。搭建 UsersModule,提供最小可用接口:POST /users、GET /users、GET /users/:id。建立你对 Nest 核心分层的第一套心智模型:Module -> Controlle…

本文属于 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 依赖的场景吗,怎么处理

会,而且很常见。处理方式是:

  1. 依赖方在 imports 声明被依赖模块。
  2. 被依赖模块在 exports 暴露必要 Provider。
  3. 不跨模块访问未导出的内部实现。
  4. 循环依赖短期可用 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 时,执行路径是:

  1. 请求进入 UsersController.create。
  2. Controller 调用 UsersService.create。
  3. Service 处理业务并返回结果。
  4. 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

返回课程目录

下一课:DTO 与 ValidationPipe 参数校验

— 感谢阅读 —