Nest 第一步 · 第 3 篇:理解 Controller / Service / Module 三层架构

这是《Nest 第一步》系列的第三篇。

前两篇我们完成了项目搭建和数据库连接。这一篇,我们来理解 NestJS 最核心的设计------三层架构

用前端开发来理解:

  • Controller 就像路由 (相当于前端的 router.js),定义"哪个 URL 对应哪个处理函数"
  • Service 就像工具函数 (相当于前端的 utils/),封装具体的业务逻辑
  • Module 就像文件组织 (相当于前端的 index.js 汇总导出),把相关代码放在一起

搞懂这三层,你就能读懂 NestJS 项目的代码结构,知道"代码应该往哪写"。

一、三层架构图解

我们先用一张图,看清楚三层架构的数据流向:

graph LR A[&#34;前端请求<br>GET /users&#34;] --> B[&#34;Controller 层<br>app.controller.ts<br>@Get('users')&#34;] B --> C[&#34;Service 层<br>app.service.ts<br>getUserList()&#34;] C --> D[&#34;Prisma 层<br>prisma.service.ts<br>user.findMany()&#34;] D --> E[&#34;数据库<br>Neon PostgreSQL<br>User 表&#34;] E --> F[&#34;返回数据<br>用户列表 JSON&#34;] F --> G[&#34;前端<br>渲染页面&#34;] style A fill:#e1f5fe,stroke:#01579b style B fill:#fff3e0,stroke:#e65100 style C fill:#e8f5e9,stroke:#1b5e20 style D fill:#f3e5f5,stroke:#4a148c style E fill:#fff9c4,stroke:#f57f17 style F fill:#e1f5fe,stroke:#01579b style G fill:#e1f5fe,stroke:#01579b

这张图告诉我们什么?

步骤 谁在做 做什么
1 前端 发起 HTTP 请求 GET /users
2 Controller 接收请求,调用 Service
3 Service 处理业务逻辑,调用 Prisma 查数据库
4 Prisma 执行 SQL,从数据库取数据
5 Service 把数据返回给 Controller
6 Controller 把数据封装成 JSON 返回给前端
7 前端 收到 JSON,渲染页面

二、Controller 层:路由和请求处理

前端类比:Controller 就像前端的 router.js ,定义"哪个 URL 对应哪个处理函数"

2.1 什么是 Controller?

在 NestJS 中,Controller(控制器) 的职责是:

  1. 接收 HTTP 请求(GET、POST、PATCH、DELETE 等)
  2. 解析请求参数(URL 参数、请求体、查询参数等)
  3. 调用 Service 处理业务
  4. 返回响应给客户端

2.2 我们现有的 Controller 代码走读

typescript 复制代码
// ────────────────────────────────────────────────────────────
// 1. 导入依赖
// ────────────────────────────────────────────────────────────

// Controller: 标记这个类是一个控制器
// Get, Post, Patch, Delete: HTTP 方法装饰器
// Body, Param: 参数装饰器,用于获取请求中的数据
import {
  Controller,
  Get,
  Post,
  Patch,
  Delete,
  Body,
  Param,
} from '@nestjs/common';

// 导入 Service(业务逻辑层)
import { AppService, UserService } from './app.service.js';

// ────────────────────────────────────────────────────────────
// 2. 控制器类
// ────────────────────────────────────────────────────────────

// @Controller() 装饰器:告诉 NestJS 这个类是一个控制器
// 如果写上前缀如 @Controller('api'),所有路由都会变成 /api/xxx
@Controller()
export class AppController {
  // 构造函数:依赖注入 Service
  // 相当于"前台配了一个对讲机,可以直接联系后台"
  constructor(
    private readonly appService: AppService,
    private readonly userService: UserService,
  ) {}

  // ──────────────────────────────────────────────────────────
  // 路由:GET /
  // ──────────────────────────────────────────────────────────

  // @Get() 装饰器:处理 GET 请求
  // 不传路径表示根路径:/
  // 相当于前端路由:{ path: '/', component: ... }
  @Get()
  getHello(): string {
    // 调用 Service 的方法,返回结果
    return this.appService.getHello();
  }

  // ──────────────────────────────────────────────────────────
  // 路由:GET /users
  // ──────────────────────────────────────────────────────────

  // @Get('users'):处理 GET /users 请求
  // 相当于前端路由:{ path: '/users', component: ... }
  @Get('users')
  async getUsers() {
    // 调用 UserService 的 getUserList 方法
    // 由 Service 层去数据库查询所有用户
    return this.userService.getUserList();
  }

  // ──────────────────────────────────────────────────────────
  // 路由:GET /users/:id
  // ──────────────────────────────────────────────────────────

  // @Get('users/:id'):处理 GET /users/1 这样的请求
  // :id 是动态参数,可以匹配 /users/1、/users/2 等
  @Get('users/:id')
  async getUser(@Param('id') id: string) {
    // @Param('id'):从 URL 中提取 id 参数
    // 例如请求 GET /users/1 → id = '1'
    // 然后传给 Service,转成数字类型
    return this.userService.getUserDetail(Number(id));
  }

  // ──────────────────────────────────────────────────────────
  // 路由:POST /users
  // ──────────────────────────────────────────────────────────

  // @Post('users'):处理 POST /users 请求
  // 用于创建新用户
  @Post('users')
  async createUser(@Body() body: { email: string; name: string }) {
    // @Body():从请求体中提取整个 body 对象
    // 前端 POST /users 时,在请求体里传 JSON
    // 直接传给 Service 处理
    return this.userService.createUser(body);
  }

  // ──────────────────────────────────────────────────────────
  // 路由:PATCH /users/:id
  // ──────────────────────────────────────────────────────────

  // @Patch('users/:id'):处理 PATCH /users/1 这样的请求
  // 用于部分更新用户信息
  @Patch('users/:id')
  async updateUser(
    @Param('id') id: string,
    @Body() body: { email?: string; name?: string },
  ) {
    // 从 URL 取 id,从 body 取要更新的字段
    // 传给 Service 执行更新
    return this.userService.updateUser(Number(id), body);
  }

  // ──────────────────────────────────────────────────────────
  // 路由:DELETE /users/:id
  // ──────────────────────────────────────────────────────────

  @Delete('users/:id')
  async deleteUser(@Param('id') id: string) {
    return this.userService.deleteUser(Number(id));
  }
}

2.3 Controller 常用装饰器总结

装饰器 用途 示例
@Controller() 标记类为控制器 @Controller('api') → 所有路由加 /api 前缀
@Get() 处理 GET 请求 @Get('list') → GET /list
@Post() 处理 POST 请求 @Post() → POST /
@Patch() 处理 PATCH 请求 @Patch(':id') → PATCH /:id
@Delete() 处理 DELETE 请求 @Delete(':id') → DELETE /:id
@Param() 获取 URL 路径参数 @Param('id') id → 从 /users/1 取出 1
@Query() 获取 URL 查询参数 @Query('page') page → 从 ?page=2 取出 2
@Body() 获取请求体 @Body() body → 获取整个 JSON body

2.4 前端对照表

前端概念 NestJS 对应
router.js 定义路由 @Get()@Post() 装饰器
useParams() 获取 URL 参数 @Param() 装饰器
useSearchParams() 获取查询参数 @Query() 装饰器
fetch() 的 body @Body() 装饰器

三、Service 层:业务逻辑

前端类比:Service 就像前端的 utils/api/ 文件夹,封装具体的业务逻辑

3.1 什么是 Service?

Service(服务) 的职责是:

  1. 处理具体的业务逻辑
  2. 操作数据库(通过 Prisma)
  3. 调用外部 API
  4. 处理数据加工

简单说:Service 是"后台"------Controller 接到订单(请求)后,告诉 Service "去做事情",Service 做好后把(数据)返回来。

3.2 为什么要有 Service 层?

如果不分 Service 如果分了 Service
所有代码都写在 Controller 里 Controller 只负责路由和参数解析
同一个业务逻辑被多个 Controller 重复写 一个 Service 被多个 Controller 复用
改业务逻辑要改 Controller,改路由也要改 Controller 改业务逻辑只改 Service,Controller 不动

核心原则:Controller 只管"接"和"返",Service 只管"做"。

3.3 我们现有的 Service 代码走读

typescript 复制代码
// ────────────────────────────────────────────────────────────
// 1. 导入依赖
// ────────────────────────────────────────────────────────────

// Injectable: 标记类可以被依赖注入
import { Injectable } from '@nestjs/common';

// PrismaService: 数据库操作服务
import { PrismaService } from './prisma.service.js';

// UserModel: Prisma 生成的类型,用于类型检查
import type { UserModel } from './generated/prisma/models.js';

// ────────────────────────────────────────────────────────────
// 2. AppService:应用基础服务
// ────────────────────────────────────────────────────────────

@Injectable()
export class AppService {
  // 这是一个同步方法,直接返回字符串
  getHello(): string {
    return 'Hello World!';
  }
}

// ────────────────────────────────────────────────────────────
// 3. UserService:用户业务服务
// ────────────────────────────────────────────────────────────

@Injectable()
export class UserService {
  // 构造函数:注入 PrismaService
  // 相当于"后厨配了一个冰箱(数据库),可以直接取食材"
  constructor(private readonly prisma: PrismaService) {}
  
  // 下方这些方法被 Controller 调用
  // 方法负责"去数据库" 把用户查出来或编辑、删除用户

  // ──────────────────────────────────────────────────────────
  // 业务方法:查询所有用户
  // ──────────────────────────────────────────────────────────
  async getUserList(): Promise<UserModel[]> {
    return this.prisma.user.findMany();
  }

  // ──────────────────────────────────────────────────────────
  // 业务方法:根据 ID 查询单个用户
  // ──────────────────────────────────────────────────────────

  async getUserDetail(id: number): Promise<UserModel | null> {
    return this.prisma.user.findUnique({
      where: { id },
    });
  }

  // ──────────────────────────────────────────────────────────
  // 业务方法:创建用户
  // ──────────────────────────────────────────────────────────

  async createUser(data: { email: string; name: string }): Promise<UserModel> {
    return this.prisma.user.create({
      data,
    });
  }

  // ──────────────────────────────────────────────────────────
  // 业务方法:更新用户
  // ──────────────────────────────────────────────────────────

  async updateUser(
    id: number,
    data: { email?: string; name?: string },
  ): Promise<UserModel> {
    return this.prisma.user.update({
      where: { id },
      data,
    });
  }

  // ──────────────────────────────────────────────────────────
  // 业务方法:删除用户
  // ──────────────────────────────────────────────────────────

  async deleteUser(id: number): Promise<UserModel> {
    return this.prisma.user.delete({
      where: { id },
    });
  }
}

3.4 Prisma 常用方法速查表

在 Service 里操作数据库时,最常用的是 Prisma 提供的这 5 个方法:

Prisma 方法 用途 对应 SQL
findMany() 查询多条记录 SELECT * FROM "User"
findUnique() 根据唯一字段查询单条记录 SELECT * FROM "User" WHERE id = ?
create() 创建一条记录 INSERT INTO "User" ...
update() 更新一条记录 UPDATE "User" SET ... WHERE id = ?
delete() 删除一条记录 DELETE FROM "User" WHERE id = ?

其他常用方法:

Prisma 方法 用途
findFirst() 查询符合条件的第一条记录
count() 统计记录数量
upsert() 存在则更新,不存在则创建
deleteMany() 删除多条记录
updateMany() 更新多条记录

3.5 前端对照表

前端概念 NestJS 对应
utils/ 工具函数 Service 里的方法
api/ 接口封装 Service 里调用 Prisma
状态管理里的 actions Service 里的业务方法

四、Module 层:组织代码

前端类比:Module 就像前端的 index.js 汇总导出,把相关代码组织在一起

4.1 什么是 Module?

Module(模块) 的职责是:

  1. 把 Controller 和 Service 组织在一起
  2. 注册所有可被依赖注入的类
  3. 导入其他模块的功能

简单说:Module 是"公司组织架构图"------它告诉 NestJS:这个模块里有哪些前台(Controller)、哪些后台(Service)。

4.2 我们现有的 Module 代码走读

typescript 复制代码
// ────────────────────────────────────────────────────────────
// 1. 导入依赖
// ────────────────────────────────────────────────────────────

// Module: 标记类为模块
import { Module } from '@nestjs/common';

// 控制器:前台负责处理 HTTP 请求
import { AppController } from './app.controller.js';

// 服务:后台负责业务逻辑
import { AppService, UserService } from './app.service.js';

// 数据库服务
import { PrismaService } from './prisma.service.js';

// ────────────────────────────────────────────────────────────
// 2. 根模块 AppModule
// ────────────────────────────────────────────────────────────

@Module({
  // imports: 导入其他模块(暂无)
  // 相当于"引用外部公司的部门"
  imports: [],

  // controllers: 注册控制器(前台)
  // 告诉 NestJS 这个模块有哪些前台接待
  controllers: [AppController],

  // providers: 注册服务(后台)
  // 告诉 NestJS 这个模块有哪些后台
  // 所有在 providers 中注册的类,都可以被"依赖注入"到其他类中
  providers: [
    AppService,     // 基础服务
    UserService,    // 用户业务服务
    PrismaService,  // 数据库服务
  ],
})
export class AppModule {}

4.3 Module 的三个核心属性

属性 作用 类比
imports 导入其他模块的功能 "借用其他部门的资源"
controllers 注册控制器(路由) "前台接待名单"
providers 注册服务(可被注入) "后台员工名单"

动手实践:按邮箱查询用户

5.1 在 Service 中添加方法

src/app.service.tsUserService 中添加:

csharp 复制代码
// ──────────────────────────────────────────────────────────
// 业务方法:根据邮箱查询单个用户
// ──────────────────────────────────────────────────────────

// 对应路由:GET /users/email/:email
// 例如:GET /users/email/user1@zhang.com
async getUserByEmail(email: string): Promise<UserModel | null> {
  // Prisma 执行 SQL:SELECT * FROM "User" WHERE email = ?;
  return this.prisma.user.findUnique({
    where: { email },
  });
}

5.2 在 Controller 中添加路由

src/app.controller.tsAppController 中添加:

less 复制代码
// ──────────────────────────────────────────────────────────
// 路由:GET /users/email/:email
// ──────────────────────────────────────────────────────────

@Get('users/email/:email')
async getUserByEmail(@Param('email') email: string) {
  return this.userService.getUserByEmail(email);
}

5.3 测试

浏览器访问 http://localhost:3000/users/email/user1@zhang.com

六、总结

三层职责一句话概括

层级 一句话概括 前端类比
Controller 定义 URL 和接收请求 路由配置
Service 写具体的业务逻辑 工具函数 / API 封装
Module 把 Controller 和 Service 组装起来 模块导出汇总

代码组织原则

复制代码
Controller 收到请求 → 交给 Service 处理 → 返回结果给客户端

记住:

  • Controller 里不要写业务逻辑(只做路由和参数处理)
  • Service 里不要写路由(只做业务逻辑)
  • Module 里注册所有需要用的东西

欢迎关注,一起从 React/Vue 走向全栈。

相关推荐
用户78136671144517 分钟前
RGW 对象多版本功能系统架构与代码解析
后端
学长毕业设计18 分钟前
基于SpringBoot的校园爱心志愿管理系统(源码+文档+讲解视频)
vue.js·spring boot·后端
IT_陈寒1 小时前
SpringBoot自动配置失效?这个隐式依赖坑了我三天
前端·人工智能·后端
码视野1 小时前
基于 Spring Boot + Vue3 的【城市雨污水管网液位淤积溯源与立交桥下穿隧洞防汛排涝智控中台】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端
小满zs1 小时前
Go语言第十章(指针)
后端·google·go
十正1 小时前
用 HTTP 条件请求把“强一致“和“低成本
网络·后端·http
2601_962122971 小时前
Spring Cloud——路由网关Zuul
后端·spring·spring cloud
卷无止境1 小时前
从一杯咖啡的订单说起,数据库设计到底是怎么长出来的
后端·python
再吃一根胡萝卜2 小时前
Rust 桌面宠物拖拽踩坑实录:重影、不跟手、置顶失效
后端