NestJS 入门(1):先搞懂 Module、Controller、Service

NestJS 初学最容易懵的不是语法,而是:一个请求进来之后,代码到底怎么分层?

你可以先记住一句话:

Controller 接请求,Service 做业务,Module 把它们组装起来。

下面用一套真实后端代码,把这三层拆开看。


1. 入口:应用从哪里启动?

Nest 应用启动时,会创建一个 App,并把根模块挂上去:

typescript 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();
  await app.listen(3000);
}
bootstrap();

真正业务代码不写在 main.ts 里。main.ts 只负责:

  1. 创建应用
  2. 挂全局配置(CORS、异常处理等)
  3. 监听端口

业务能力都在各个 Module 里。


2. Module:功能的「收纳盒」

根模块通常长这样:

typescript 复制代码
import { Module } from '@nestjs/common';
import { AuthModule } from './modules/auth/auth.module';
import { DocumentsModule } from './modules/documents/documents.module';
import { HealthController } from './health.controller';

@Module({
  imports: [
    AuthModule,
    DocumentsModule,
    // ... 其他业务模块
  ],
  controllers: [HealthController],
})
export class AppModule {}

读这段时抓住三点:

  • imports:引入其他模块(认证、文档......)
  • controllers:本模块对外暴露的路由
  • 每个业务能力尽量一个 Module,避免全塞进 AppModule

健康检查这种极简接口,也可以直接挂在根模块:

typescript 复制代码
import { Controller, Get } from '@nestjs/common';

@Controller()
export class HealthController {
  @Get('health')
  health() {
    return { status: 'ok', timestamp: new Date().toISOString() };
  }
}

访问 GET /health,就会走到这里。这是理解 Controller 的最小例子。


3. Controller:只负责「接请求」

再看一个更完整的认证 Controller:

typescript 复制代码
import { Controller, Post, Get, Body, Headers, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';

@Controller('api/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  login(@Body() body: { email: string; password: string }) {
    return this.authService.login(body.email, body.password);
  }

  @Post('refresh')
  refresh(@Body() body: { refreshToken: string }) {
    return this.authService.refreshToken(body.refreshToken);
  }

  @UseGuards(JwtAuthGuard)
  @Get('me')
  me(@Headers('authorization') auth: string) {
    const token = auth?.replace('Bearer ', '');
    return this.authService.validateToken(token);
  }
}

这里有几个关键点:

写法 含义
@Controller('api/auth') 路由前缀
@Post('login') 完整路径是 POST /api/auth/login
@Body() 取请求体
constructor(private readonly authService: AuthService) 依赖注入:Nest 帮你创建并传入 Service
@UseGuards(JwtAuthGuard) 进这个接口前先做鉴权

注意:Controller 几乎不写业务细节

密码对不对、怎么发 token,全部丢给 AuthService

CRUD 场景也一样,比如文档接口:

typescript 复制代码
@Controller()
export class DocumentsController {
  constructor(private readonly documentsService: DocumentsService) {}

  @Get('api/projects/:projectId/documents')
  findAll(@Param('projectId') projectId: string) {
    return this.documentsService.findAll(projectId);
  }

  @Post('api/projects/:projectId/documents')
  create(
    @Param('projectId') projectId: string,
    @Body() data: { title: string; content: string }
  ) {
    return this.documentsService.create(projectId, data);
  }
}

模式始终是:取参数 → 调 Service → 返回结果


4. Service:业务真正发生的地方

Service 用 @Injectable() 标记,表示可被注入:

typescript 复制代码
import { Injectable, UnauthorizedException } from '@nestjs/common';

@Injectable()
export class AuthService {
  async login(email: string, password: string) {
    const user = this.findUserByEmail(email);

    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const { accessToken, refreshToken } = await this.generateTokens(user);
    await this.createSession(user.id, refreshToken);

    const { password: _pwd, ...userWithoutPassword } = user;
    return {
      accessToken,
      refreshToken,
      user: userWithoutPassword,
    };
  }
}

登录流程都在 Service 里:

  1. 查用户
  2. 校验密码
  3. 签发 token / 建会话
  4. 返回数据(去掉密码字段)

Controller 只需要一行:

typescript 复制代码
return this.authService.login(body.email, body.password);

这也是 Nest 推荐的分层:

  • Controller:HTTP 细节(路径、方法、参数)
  • Service:业务规则(校验、持久化、发 token)

5. 再回到 Module:把零件装起来

有了 Controller 和 Service,还要告诉 Nest:「它们属于同一个功能包」:

typescript 复制代码
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';

@Module({
  imports: [
    // JwtModule、PassportModule、PrismaModule 等依赖
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtAuthGuard],
  exports: [AuthService, JwtAuthGuard],
})
export class AuthModule {}

对应关系可以这样记:

  • controllers:谁对外提供 HTTP 接口
  • providers:谁提供可注入能力(Service、Guard......)
  • exports:哪些东西允许被其他模块继续用
  • imports:本模块依赖哪些外部能力

文档模块更「典型」一点:

typescript 复制代码
@Module({
  imports: [PrismaModule],
  controllers: [DocumentsController],
  providers: [DocumentsService],
  exports: [DocumentsService],
})
export class DocumentsModule {}

读到这里,一张请求链路就清楚了:

text 复制代码
HTTP 请求
  → Controller(解析路由/参数)
    → Service(执行业务)
      → 数据库 / 缓存 / 外部服务
    ← 返回业务结果
  ← 返回 HTTP 响应

Module 负责把这条链路上的类注册好,Nest 的依赖注入再负责把它们「接上电」。


6. 一张图串起来

text 复制代码
AppModule
 ├─ HealthController          → GET /health
 ├─ AuthModule
 │   ├─ AuthController        → POST /api/auth/login
 │   └─ AuthService           → 校验账号、签发 token
 └─ DocumentsModule
     ├─ DocumentsController   → GET/POST /api/projects/:id/documents
     └─ DocumentsService      → 查库、创建、更新

学 Nest 时,建议每看到一个新功能,都先问三句:

  1. 哪个 Controller 接这个请求?
  2. 哪个 Service 处理业务?
  3. 它们被哪个 Module 组装?

能答出来,入门就过半了。


7. 小结

  • Nest 不是「把 Express 路由堆在一个文件里」,而是按模块组织。
  • Controller 薄、Service 厚:这是最值得先养成的习惯。
  • Module 是装配车间:声明依赖、暴露接口、控制边界。
  • 先读懂一个完整小模块(比如 auth),再扩到 CRUD,比上来堆装饰器有效得多。

系列导航

相关推荐
XuCoder1 小时前
Redis 分片集群:它到底是怎么把数据分片又路由的?
后端
吃饱了得干活1 小时前
Java Map 核心原理:从数据结构到 put/get 执行,一篇彻底讲透
java·后端
hy.z_7771 小时前
【C++】9. Vector
开发语言·c++
不才不才不不才1 小时前
Spring 源码系列(17): HandlerMapping 与 HandlerAdapter 两大体系
java·后端·spring
Seven971 小时前
AI杂谈:别再问AI会不会替代你,先看你是不是驾驶员
人工智能·后端
Tim_102 小时前
【C++】023、移动语义&深拷贝
开发语言·c++·算法
ERD Online2 小时前
我们怎么设计 good first issue:让第一个 PR 两小时内合入
数据库·git·后端·开源·issue
ctlover2 小时前
Python模块与包
开发语言·python
雨田言炎2 小时前
十、QThread多线程
linux·服务器·开发语言·前端·qt
IT_陈寒2 小时前
Vite热更新失效?我的几个犯傻操作害我debug两小时
前端·人工智能·后端