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 只负责:
- 创建应用
- 挂全局配置(CORS、异常处理等)
- 监听端口
业务能力都在各个 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 里:
- 查用户
- 校验密码
- 签发 token / 建会话
- 返回数据(去掉密码字段)
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 时,建议每看到一个新功能,都先问三句:
- 哪个 Controller 接这个请求?
- 哪个 Service 处理业务?
- 它们被哪个 Module 组装?
能答出来,入门就过半了。
7. 小结
- Nest 不是「把 Express 路由堆在一个文件里」,而是按模块组织。
- Controller 薄、Service 厚:这是最值得先养成的习惯。
- Module 是装配车间:声明依赖、暴露接口、控制边界。
- 先读懂一个完整小模块(比如 auth),再扩到 CRUD,比上来堆装饰器有效得多。