🚀 AI全栈项目实战第四天:React 玩家的后端新大陆 —— NestJS 深度硬核指南

128 阅读12分钟

哈喽,掘金的各位全栈练习生们!我是你们的神秘猪头导师。欢迎来到 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。
  • 现在 ?? 怎么做? 只有当左侧是 nullundefined 时,才会使用右侧的值。如果左侧是 0false,它会照单全收。这就是严谨!

🧠 第三步:大脑中枢 —— 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 接收一个对象,里面有三个核心属性:

  1. imports: “我需要谁帮忙?” 这里引入了 TodosModuleDatabaseModule。说明 AppModule 这个老板可以调用这两个部门的能力。
  2. controllers: “谁负责接待客人?” AppController 是门面,负责处理 HTTP 请求。
  3. 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
  ){}

这里有两个非常硬核的知识点:

  1. TypeScript 的参数属性 (Parameter Properties): 你看到 private readonly appService: AppService 了吗? 在普通的 JS 类里,你需要先定义 this.appService,然后在构造函数里 this.appService = appService。 但在 TS 里,只要你在构造函数参数前加上 privatepublicreadonly,TS 就会自动帮你把这个参数变成类的属性,并自动赋值!一行代码顶三行,简洁就是正义。

  2. 依赖注入 (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 类型定义。在函数内部,我们可以直接解构 usernamepassword 进行校验。虽然这里我们是手动写 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 这行代码做了一件很酷的事:
    1. 从路由提取 id
    2. 中间件拦截ParseIntPipe 尝试把它转成整数。
    3. 如果转换失败(比如传了 abc),NestJS 直接抛出 400 错误,甚至不会进入函数体。
    4. 如果成功,传入函数的 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 的核心命脉:

  1. CLI 就像魔法棒nest new 一键生成。
  2. Main 入口:工厂模式创建应用,?? 运算符保驾护航。
  3. Module 乐高积木@Module 组装 Controller 和 Service。
  4. Controller 接待员:路由分发,参数校验,Pipe 管道清洗数据。
  5. Service 大厨@Injectable 承载业务逻辑。
  6. Dependency Injection (DI):通过构造函数自动注入依赖,解放双手。
  7. Custom Provider:利用 Token 注入数据库连接池等第三方实例。

NestJS 就像是给 Node.js 穿上了一套钢铁侠的战衣。它可能一开始看起来有点重,但当你飞起来的时候,你会感谢这套战衣给你提供的强大动力和安全感。

对于 React 开发者来说,学习 NestJS 是一次思维的升维。你不再只是画页面的画家,你是构建世界的建筑师。

Stay Hungry, Stay Foolish, See you next time! 👋


💡 附录:核心代码地图