哈喽,掘金的各位全栈练习生们!我是你们的神秘猪头导师。欢迎来到 AI 全栈项目实战的第四天!👋
在上一篇文章中,我们可能还在纠结前端页面的像素眼,或者用原生的 Node.js 手搓简单的服务。但今天,我们要进入一个更加深邃、更加迷人,也绝对会让 React 开发者感到无比亲切的领域——NestJS。
如果你觉得 Express 像是一个“只有毛坯房”的装修现场,什么都要自己造;那么 NestJS 就是精装修的“豪宅”,水电煤气(路由、依赖注入、模块化)一应俱全,你只需要拎包入住,专注于你的业务逻辑家具摆放。
准备好了吗?我们要开始一场从“前端切图仔”到“企业级后端架构师”的华丽转身了!🏎️
🧐 为什么是 NestJS?
很多 React 开发者第一次看 NestJS 的代码时,都会惊呼:“这味道……太熟悉了!”
1. 模块化架构 (Modular) 🧩
React 用 Component(组件)来拼凑 UI,NestJS 用 Module(模块) 来拼凑后端服务。你的代码不再是散落在文件夹里的意大利面条,而是井井有条的积木。
2. 依赖注入 (Dependency Injection) 💉
听起来很吓人?其实就是“饭来张口”。你需要数据库连接?不需要你自己去 new Database(),只需要在构造函数里喊一声,NestJS 的底层容器就会把数据库实例“注入”给你。这大大降低了代码的耦合度。
3. TypeScript 的亲儿子 📘
NestJS 原生就是用 TypeScript 写的。对于我们这些习惯了类型约束、受够了 undefined is not a function 的现代前端人来说,这简直就是福音。
4. MVC 模式的完美落地 🏗️
- M (Model): 数据层(Service/Database)
- V (View): 视图层(API 接口返回的 JSON)
- C (Controller): 控制层(路由分发)
🛠️ 第一步:起高楼 —— 项目创建
话不多说,先跑起来。打开你的终端(Terminal),我们先装上 NestJS 的脚手架。这就像是买了一把万能钥匙。
# 安装 NestJS CLI 全局工具
npm i -g @nestjs/cli
# 新建项目,我们要创建一个叫 nest-test-demo 的项目
nest new nest-test-demo
在这个过程中,CLI 会问你用 npm 还是 yarn,选你喜欢的就好。等进度条跑完,一个企业级架构的后端项目就已经躺在你的硬盘里了。
🚪 第二步:叩开真理之门 —— main.ts 入口分析
我们先不看别的,直接冲进 main.ts。这里是整个程序的入口,梦开始的地方。
import { NestFactory } from '@nestjs/core';
// 模块化
import { AppModule } from './app.module';
import { config } from 'dotenv';
config();
async function bootstrap() {
// server app
// 工厂模式 三星 手机,汽车,方便面
// NestFactory nest 工厂
// 根模型
const app = await NestFactory.create(AppModule); // L12
// 3000 node 进程对象process
// 空值合并运算符 ES2020 2015是ES6所以2020是ES11
await app.listen(process.env.PORT ?? 3000); // L15
}
bootstrap();
🏭 知识点 1:工厂模式 (Factory Pattern)
请看第 12 行:
const app = await NestFactory.create(AppModule);
这里用到了经典的设计模式——工厂模式。
想象一下,你想要一台三星手机。你不需要自己去采购屏幕、芯片、电池然后自己组装(那是 new App() 做的事)。你只需要找到“三星工厂” (NestFactory),告诉它:“给我造一台手机!” (create),并且把设计图纸 (AppModule) 给它。
NestFactory 就会帮你把底层的 Express 实例、路由系统、异常过滤器等等全部组装好,直接返给你一个可以直接使用的 app 对象。这就是封装的艺术。
❓ 知识点 2:空值合并运算符 (Nullish Coalescing)
再看第 15 行:
await app.listen(process.env.PORT ?? 3000);
这里有个可爱的小符号 ??。这是 ES2020 (ES11) 引入的新特性,叫空值合并运算符。
- 以前我们怎么写?
process.env.PORT || 3000。但这有个 bug,如果端口真的是0(虽然很少见),||会把它当成 false,强行变成 3000。 - 现在
??怎么做? 只有当左侧是null或undefined时,才会使用右侧的值。如果左侧是0或false,它会照单全收。这就是严谨!
🧠 第三步:大脑中枢 —— App.Module
如果说 main.ts 是大门,那 app.module.ts 就是总指挥部。NestJS 是模块化的,而 AppModule 是所有模块的根(Root Module)。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';
import { DatabaseModule } from './database/database.module';
// mvc 设计模式 模型-视图-控制器
// 一个文件一个类
// 装饰器模式 让AppModule类成为一个模块
@Module({
imports: [
TodosModule,
DatabaseModule
],
// 后端路由 控制逻辑 参数校验 逻辑处理
controllers: [AppController],
// 后端服务 业务逻辑 数据库操作
// 数据
providers: [AppService],
})
export class AppModule {}
🎀 知识点 3:装饰器 (Decorators)
看到那个 @Module 了吗?这就是装饰器。
在 ES6 的类(Class)上面加个 @,就像给这个类贴了个魔法标签。
AppModule 本身只是个空类(Class),啥也不会。但是贴上 @Module 后,NestJS 就知道:“哦!这是一个模块!”
@Module 接收一个对象,里面有三个核心属性:
- imports: “我需要谁帮忙?” 这里引入了
TodosModule和DatabaseModule。说明AppModule这个老板可以调用这两个部门的能力。 - controllers: “谁负责接待客人?”
AppController是门面,负责处理 HTTP 请求。 - providers: “谁负责干活?”
AppService是苦力,负责具体的业务逻辑。
这三者共同构成了 NestJS 的 MVC 架构基石。
🗣️ 第四步:门面担当 —— Controller (控制器)
接下来我们看看负责接待的 AppController。它的职责非常明确:监听路由,接收参数,校验数据,然后把活儿派给 Service。
import { Controller,Get,Post,Body,Inject } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
// service 实例
constructor(
@Inject('PG_CONNECTION') private readonly db: any, // L8
private readonly appService: AppService // L9
){}
@Get()
getHello(): string {
return this.appService.getHello();
}
// ... 省略部分代码
@Post('login')
login(@Body() body: {username: string, password: string}){
const {username,password} = body;
console.log(username,password);
// ... 参数校验逻辑
return this.appService.handleLogin(username,password);
}
}
💉 知识点 4:构造函数注入 (Constructor Injection) —— 核心难点!
请大家把目光聚焦在第 7-10 行的 constructor:
constructor(
@Inject('PG_CONNECTION') private readonly db: any,
private readonly appService: AppService
){}
这里有两个非常硬核的知识点:
-
TypeScript 的参数属性 (Parameter Properties): 你看到
private readonly appService: AppService了吗? 在普通的 JS 类里,你需要先定义this.appService,然后在构造函数里this.appService = appService。 但在 TS 里,只要你在构造函数参数前加上private、public或readonly,TS 就会自动帮你把这个参数变成类的属性,并自动赋值!一行代码顶三行,简洁就是正义。 -
依赖注入 (DI) 的真谛: 我们没有写
new AppService()。我们只是声明了类型: AppService。NestJS 的扫描机制发现你需要AppService,它就会去容器里找,找到之前在AppModule里注册过的那个AppService实例,直接塞给你。 这就叫“雇佣兵”模式:谁需要谁招募,但招募的动作由系统自动完成。至于那个
@Inject('PG_CONNECTION'),我们稍后讲数据库模块时会揭晓它的神秘面纱!🎭
🚦 知识点 5:HTTP 动词与参数装饰器
-
@Controller(): 可以传参,比如
@Controller('users'),那下面的路由前缀就是/users。这里没传,就是根路径。 -
@Get(): 处理 GET 请求。
-
@Post('login'): 处理 POST 请求,路径是
/login。 -
@Body(): 这是一个参数装饰器。它专门用来提取 HTTP 请求体(Body)里的数据。
login(@Body() body: {username: string, password: string})这里我们还顺手给
body加了个 TS 类型定义。在函数内部,我们可以直接解构username和password进行校验。虽然这里我们是手动写if (!username)来校验的,但在更高级的用法里,我们可以配合 DTO (Data Transfer Object) 和class-validator来自动校验。
Controller 做完校验后,最后一行 return this.appService.handleLogin(...),就把具体的登录逻辑甩锅给 Service 了。这就叫职责分离!
👨🍳 第五步:幕后大厨 —— Service (服务)
Controller 只是服务员,拿着菜单(参数)进厨房。AppService 才是真正炒菜的大厨。
// 依赖注入
import { Injectable } from '@nestjs/common'
// controller 服务于路由, 树根
// service 店里的厨师 Injectable 被注入
@Injectable()
export class AppService {
getHello(): string {
return '你好yeah';
}
// ...
handleLogin(username: string,password: string) {
if(username === "admin" && password === "123456"){
return { code: 200, msg: '登录成功' };
}
else return { code: 400, msg: '用户名或密码错误' };
}
}
🏷️ 知识点 6:@Injectable()
@Injectable() 顾名思义:可被注入的。
这个装饰器告诉 NestJS:“嘿,我是个有用的类,我可以被注入到别的 Controller 或者其他 Service 里去。”
所有的业务逻辑(Business Logic),比如计算、判断密码、复杂的算法,都应该写在这里。Controller 应该保持清爽,只负责收发。
🧱 第六步:举一反三 —— Feature Module (Todos 业务模块)
看完了 AppModule,我们来看看如何扩展业务。比如我们要写一个待办事项(Todo)的功能。NestJS 推荐我们将功能按模块拆分。
1. Todos Module
@Module({
controllers: [TodosController],
providers: [TodosService],
})
export class TodosModule {}
结构和 AppModule 一模一样!这就是模块化的美妙之处——一致性。无论你的项目多大,每个模块的结构都是可预测的。
2. Todos Controller (解锁新技能)
在 todos.controller.ts 里,我们解锁了几个新装饰器:
import { Controller, Get, Post, Body, Delete, Param, ParseIntPipe } from '@nestjs/common';
// ...
@Controller('todos') // 路由前缀,访问路径变成了 /todos
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Get()
getTodos() { return this.todosService.findAll(); }
@Post()
addTodo(@Body('title') title: string){ // 只取 body 里的 title 字段
return this.todosService.addTodo(title);
}
@Delete(':id') // 动态路由
deleteTodo(@Param('id', ParseIntPipe) id: number){ // 管道转换
console.log(typeof id,'/////'); // 这里的 id 已经是 number 类型了!
return this.todosService.deleteTodo(id);
}
}
- @Controller('todos'): 这里加了前缀。所以
getTodos的 URL 是GET /todos。 - @Body('title'): 之前我们在
login里是获取整个 body。这里我们可以传个字符串'title',告诉 NestJS:“我只要 body 里的 title 属性”。精准打击!🎯 - @Delete(':id'): 定义了一个动态路由参数。URL 类似于
DELETE /todos/123。 - Pipe (管道) ——
ParseIntPipe: 这是 NestJS 的一大杀器!HTTP 传过来的参数默认都是 String 类型的。 但是我们的 Service 需要一个 Number 类型的 ID。@Param('id', ParseIntPipe) id: number这行代码做了一件很酷的事:- 从路由提取
id。 - 中间件拦截:
ParseIntPipe尝试把它转成整数。 - 如果转换失败(比如传了
abc),NestJS 直接抛出 400 错误,甚至不会进入函数体。 - 如果成功,传入函数的
id就是纯正的number类型。
- 从路由提取
3. Todos Service (接口与数据)
export interface Todo {
id: number;
title: string;
completed: boolean;
}
@Injectable()
export class TodosService {
private todos: Todo[] = [ ... ]; // 内存数据库
// ... CRUD 方法
}
这里我们定义了 Todo 接口(Interface)。在 TS 里,接口是构建健壮应用的基础,它规定了数据的形状。
💎 第七步:进阶硬核 —— Database Module (自定义 Provider)
最后,我们来讲讲最硬核的部分——如何封装一个数据库模块。
我们在 AppController 里看到的 @Inject('PG_CONNECTION') 到底是从哪来的?
请看 src/database/database.module.ts:
import { Module,Global } from '@nestjs/common';
import { Pool } from 'pg'; // PostgreSQL 客户端
import * as dotenv from 'dotenv';
dotenv.config();
// 数据库基础服务
@Global() // 全局服务
@Module({
providers: [
{
provide: 'PG_CONNECTION', // 自定义令牌 (Token)
// 连接池
useValue: new Pool({
user: process.env.DB_USER,
// ... 环境变量配置
port: parseInt(process.env.DB_PORT || '5432',10),
})
}
],
exports: ['PG_CONNECTION'] // 导出令牌,让别人也能用
})
export class DatabaseModule {}
这里包含了 NestJS 高级依赖注入的精髓:
1. 自定义 Provider (Custom Provider)
通常我们的 provider 是一个类(比如 AppService)。但有时候,我们需要注入一个第三方库的实例(比如 pg 的连接池 Pool),或者一个常量。
这时我们就不能直接写类名了,而是要用对象字面量:
- provide:
'PG_CONNECTION'。这是一个令牌 (Token)。你可以把它想象成一个暗号。 - useValue: 这是一个具体的值(在这里是一个配置好的数据库连接池实例)。
2. @Global() 全局模块
通常模块是隔离的。如果 TodosModule 想用数据库,它得在 imports 里导入 DatabaseModule。
但数据库是基础设施,每个模块都要用。每次都 import 太麻烦了。
加上 @Global() 装饰器,DatabaseModule 就变成了“VIP 中 P”。只要在 AppModule 里注册一次,全项目的任何地方都可以直接使用它导出的 Provider!
3. 回到 AppController 使用它
现在回到 src/app.controller.ts 的构造函数:
@Inject('PG_CONNECTION') private readonly db: any
这里必须用 @Inject('PG_CONNECTION'),因为 'PG_CONNECTION' 是一个字符串令牌,TS 无法通过类型自动推断出来(不像 AppService 是个类)。
通过这个暗号,NestJS 从容器里拿到了那个 Pool 实例,赋值给 this.db。
然后我们就可以愉快地写 SQL 了:
@Get('db-test')
async testConnection() {
const res = await this.db.query('SELECT * FROM users'); // L44
return { data: res.rows }
}
注意这里用了 async/await,因为数据库查询是异步操作。
📝 总结
恭喜你!🎉 读到这里,你已经掌握了 NestJS 的核心命脉:
- CLI 就像魔法棒:
nest new一键生成。 - Main 入口:工厂模式创建应用,
??运算符保驾护航。 - Module 乐高积木:
@Module组装 Controller 和 Service。 - Controller 接待员:路由分发,参数校验,Pipe 管道清洗数据。
- Service 大厨:
@Injectable承载业务逻辑。 - Dependency Injection (DI):通过构造函数自动注入依赖,解放双手。
- Custom Provider:利用 Token 注入数据库连接池等第三方实例。
NestJS 就像是给 Node.js 穿上了一套钢铁侠的战衣。它可能一开始看起来有点重,但当你飞起来的时候,你会感谢这套战衣给你提供的强大动力和安全感。
对于 React 开发者来说,学习 NestJS 是一次思维的升维。你不再只是画页面的画家,你是构建世界的建筑师。
Stay Hungry, Stay Foolish, See you next time! 👋
💡 附录:核心代码地图
- 入口文件:
main.ts - 根模块:
app.module.ts - 主控制器:
app.controller.ts - 业务模块:
todos.module.ts - 数据库模块:
database.module.ts