🎃 前端学了 Next.js,后端该学啥?NestJS 就是 Node 版的蜜雪冰城

9 阅读9分钟

写在前面:前几节课还在用 Next.js 搭博客搞全栈,今天画风一转——NestJS。老师说 Next.js 是全栈框架,NestJS 则是 Node 的纯后端企业级开发框架,默认 TypeScript,全面模块化。今天从 nest new 开始,用工厂模式启动应用、用模块拆分业务、用装饰器给类贴标签、用依赖注入让 Service 自动到岗,最后手搓了一个完整的 Todos CRUD。最妙的是,课堂用蜜雪冰城来类比工厂模式——NestFactory 就是 MixueFactory。以下所有代码均来自课堂真实文件。


一、NestJS 是什么:Next.js 的"后端表哥"

一句话区分:

框架定位擅长
Next.js全栈框架前端 SSR/SSG + API 路由
NestJS纯后端框架企业级 API、微服务、AI Infra

readme 开篇就定了调子:

"Next.js 全栈,NestJS 就是 Node 的纯后端企业级开发框架。默认使用 TypeScript,全面模块化思想,适合构建企业级服务。"

后端开发到底做什么?readme 列了三个方向:

  • 提供 API 接口(Web 开发)
  • 系统集成(并发、底层服务、AI Infra)
  • 微服务

安装也极其简单:

npm i -g @nestjs/cli
nest new hello

装完 CLI,一条命令创建项目。目录结构精简到令人发指:

src/
├── main.ts          # 入口文件
└── app.module.ts    # 根模块

就两个文件,一个管启动,一个管组织。NestJS 的哲学已经藏在目录结构里了——入口归入口,业务归业务


二、工厂模式:想喝奶茶,不用自己泡

NestJS 的第一个门槛不是路由,不是中间件,而是——工厂模式

readme 用了一个绝妙的比喻:

"你想喝奶茶,不用自己做(流程代码 NO),找蜜雪冰城。"

蜜雪冰城有什么?柠檬水、圣代、芋圆葡萄、冰激凌、珍珠奶茶……

你不需要知道每种饮品怎么做的,你只需要跟柜台说一句"要一杯柠檬水",工厂负责生产。这就是工厂模式的本质——调用者和生产者解耦

课堂的 1.mjs 写了一个完整的蜜雪冰城工厂:

// 蜜雪冰城产品之一:冰激凌
class IceCream {
  constructor() {
    this.name = '冰激凌';
    this.price = 3;
  }
  show() {
    console.log(`${this.name} 价格${this.price} 元`)
  }
}

class LemonTea {
  constructor() {
    this.name = '柠檬水';
    this.price = 4;
  }
  show() {
    console.log(`${this.name} 价格${this.price} 元`)
  }
}

class MilkTea {
  constructor() {
    this.name = '珍珠奶茶';
    this.price = 8;
  }
  show() {
    console.log(`${this.name} 价格${this.price} 元`)
  }
}

// 工厂类
class MixueFactory {
  static create(type) {
    switch(type) {
      case 'ice':   return new IceCream();
      case 'lemon': return new LemonTea();
      case 'milk':  return new MilkTea();
    }
  }
}

const drink1 = MixueFactory.create('ice');
drink1.show();   // 冰激凌 价格3 元
const drink2 = MixueFactory.create('lemon');
drink2.show();   // 柠檬水 价格4 元

注意一个关键设计:每个产品类都实现了相同的 show() 接口。 冰激凌 3 块、柠檬水 4 块、珍珠奶茶 8 块——价格不同,但都叫 show()。这意味着不管工厂返回哪个类,调用者都可以放心地直接调 show(),不用关心具体是哪种产品。

readme 的总结:

"由于工厂里的每个类都实现了相同的 show 接口,由工厂类生产出的类,可以放心的直接调用。"

NestJS 里这个工厂叫 NestFactorymain.ts 入口文件就一行核心代码:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  // 工厂模式:NestFactory 就是蜜雪冰城
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

NestFactory.create(AppModule)——你跟 Nest 工厂说"我要一个应用",工厂就给你造一个。你不需要知道应用内部怎么实例化的,工厂负责生产,你负责消费


三、模块化:每个 Module 就是一个"独立档口"

蜜雪冰城不是一个档口卖所有东西,而是分了冷饮档、冰品档、奶茶档。NestJS 也一样——每个业务一个 Module,互不干扰。

根模块:AppModule

app.module.ts 是总部,负责把所有子模块组装到一起:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';

@Module({
  imports: [TodosModule],         // 依赖外界的模块
  controllers: [AppController],   // 控制器:校验、简单逻辑
  providers: [AppService],        // 服务:复杂业务、数据
})
export class AppModule {}

readme 对 @Module 的注释一针见血:

"复杂,说明书,照着做。"

@Module 就是一份"部门组织架构说明书"——谁管前台(controllers),谁管后厨(providers),需要对接哪些外部部门(imports),写清楚就行。

业务模块:TodosModule

todos.module.ts 是一个独立的业务部门:

import { Module } from '@nestjs/common';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';

// 大型后端框架,MVC 视图层 不可以直接去数据库查数据
@Module({
  controllers: [TodosController],
  providers: [TodosService],
})
export class TodosModule {}

注释里有一句关键提醒:

"大型后端框架,MVC 视图层不可以直接去数据库查数据。"

这行注释道出了 NestJS 的铁律——Controller 不碰数据库,Service 不管路由。 各司其职,这就是 MVC 的核心。


四、装饰器模式:给类贴标签的艺术

NestJS 的代码里到处都是 @ 符号——@Module@Controller@Get@Post@Injectable。这些就是装饰器

readme 的解释:

"装饰器模式在不修改原有对象的前提下,动态给对象叠加额外功能。"

通俗点说——你的类是一个员工,装饰器就是工牌上贴的标签。贴上 @Controller,它就是前台接单员;贴上 @Injectable,它就是后厨师傅;贴上 @Get,它就知道这个方法要响应 GET 请求。

NestJS 把装饰器用到了极致。看看 app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    console.log('/ 的控制器');
    return this.appService.getHello();
  }
}

@Controller() 告诉 NestJS:这个类是控制器。@Get() 告诉 NestJS:这个方法响应 GET 请求。不需要写路由配置文件,不需要注册路由表——标签一贴,路由生效。


五、MVC 分层:前台接单,后厨干活

NestJS 的核心架构就是经典 MVC,但拆得极其干净:

角色文件职责蜜雪冰城类比
Controllerxx.controller.ts接收请求、参数校验、简单逻辑前台接单员
Servicexx.service.ts业务逻辑、数据操作(CRUD)后厨师傅
Modulexx.module.ts组装 Controller 和 Service店长

前台:Controller

Controller 的职责很明确——接单,然后交给后厨。

app.controller.tsgetHello() 方法就干了一件事:调用 Service。

@Get()
getHello(): string {
  console.log('/ 的控制器');
  // 响应什么内容?交给 service 层
  return this.appService.getHello();
}

注释说得很直白:

"响应什么内容?交给 service 层。"

前台不做饭。前台只负责:接单 → 转交后厨 → 把成品端给顾客。

后厨:Service

app.service.ts 更简单:

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  // 给 controller 层一个交代的
  getHello(): string {
    return 'Hello World!';
  }
}

注释里的"给 controller 层一个交代的"——后厨做完菜,给前台一个交代。分工明确,各管一摊。


六、依赖注入:员工自动到岗,不用你 new

传统开发里,Controller 需要用 Service 时得自己 import 然后 new 一个实例。NestJS 不需要。

app.controller.ts 的构造函数:

constructor(private readonly appService: AppService) {}

没有 new,没有 import 后手动实例化。你只要在构造函数里声明"我需要 AppService",NestJS 就自动帮你注入。

魔法在哪?在 @Injectable() 装饰器。

app.service.ts 头上贴了 @Injectable()

@Injectable()
export class AppService { ... }

这个标签告诉 NestJS:这个类可以被自动注入到任何需要它的地方。 Controller 需要它?注入。其他 Service 需要它?注入。不需要手动 new,不需要管生命周期——NestJS 全包了。

readme 的原话:

"@Injectable() 自动依赖注入,自动注入 controller 或任何用它的地方,controller 里的一个属性。"


七、CRUD 实战:Todos 的完整业务闭环

课堂用一个 Todos(待办事项)模块把 CRUD 跑了一遍。这是 NestJS 最经典的入门场景。

数据层:Todo 接口

todos.service.ts 定义了数据结构:

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

let todos: Todo[] = [
  { id: 1, title: '学习nestjs', completed: false },
  { id: 2, title: '学习 CRUD', completed: true },
];

两条初始数据,一明一暗:一个还没学完 NestJS,一个已经学完 CRUD。

Controller:五种 HTTP 方法的完整映射

todos.controller.ts 是今天最重磅的文件,把 RESTful API 的五种操作全用上了:

@Controller('todos')
export class TodosController {
  constructor(private readonly todosService: TodosService) {}

  // GET /todos —— 查全部
  @Get()
  findAll(): Todo[] {
    return this.todosService.findAll();
  }

  // GET /todos/:id —— 查一条
  @Get(':id')
  findOne(@Param('id') id: string): Todo {
    return this.todosService.findOne(Number(id));
  }

  // POST /todos —— 新增
  @Post()
  create(@Body('title') title: string): Todo {
    return this.todosService.create(title);
  }

  // DELETE /todos/:id —— 删除
  @Delete(':id')
  remove(@Param('id') id: string): { message: string } {
    this.todosService.remove(Number(id));
    return { message: `Todo ${id} removed` };
  }

  // PUT /todos/:id —— 修改
  @Put(':id')
  update(
    @Param('id') id: string,
    @Body() patch: Partial<Todo>,
  ): Todo {
    return this.todosService.update(Number(id), patch);
  }
}

逐个拆解:

装饰器HTTP 方法路由参数提取操作
@Get()GET/todos查全部
@Get(':id')GET/todos/1@Param('id')查一条
@Post()POST/todos@Body('title')新增
@Delete(':id')DELETE/todos/1@Param('id')删除
@Put(':id')PUT/todos/1@Param + @Body修改

注意 @Put 的参数:@Body() patch: Partial<Todo>Partial<Todo> 是 TypeScript 的工具类型——把 Todo 的所有字段变成可选的。你可以只传 { completed: true },也可以传 { title: '新标题', completed: false },灵活得很。

Service:后厨的五大菜谱

todos.service.ts 对应五个方法:

@Injectable()
export class TodosService {
  findAll(): Todo[] {
    return todos;
  }

  findOne(id: number): Todo {
    const todo = todos.find(t => t.id === id);
    if (!todo) throw new NotFoundException(`Todo ${id} not found`);
    return todo;
  }

  create(title: string): Todo {
    const todo: Todo = { id: nextId++, title, completed: false };
    todos.push(todo);
    return todo;
  }

  remove(id: number): void {
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) throw new NotFoundException(`Todo ${id} not found`);
    todos.splice(index, 1);
  }

  update(id: number, patch: Partial<Todo>): Todo {
    const todo = this.findOne(id);
    Object.assign(todo, patch);
    return todo;
  }
}

Controller 负责接单和参数校验,Service 负责干活。Controller 里一行业务逻辑都没有——这就是 readme 说的"controller 参数校验,简单逻辑,最后 return response"。

错误处理:NotFoundException

todos.service.ts 里有两处错误处理,都用的是 NestJS 内置的 NotFoundException

if (!todo) throw new NotFoundException(`Todo ${id} not found`);

readme 特别强调了错误处理的重要性:

"nestjs 提供了各种错误类,标准化错误输出。statusCode 状态码,message 消息。"

不用自己拼 JSON 错误响应,NotFoundException 一抛,NestJS 自动返回标准的 404 响应,带状态码和消息。省心。

对比一下传统方式——try catch finally,TypeScript 独苗,一个线程挂了全崩。NestJS 的内置错误类把异常处理标准化了,不用每个接口都写 try-catch


八、测试:NestJS 的出厂质检

NestJS 脚手架自带测试文件。app.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});

NestJS 用 Test.createTestingModule() 创建了一个测试专用的模块——相当于蜜雪冰城开业前的内部试吃。Controller 和 Service 都在测试模块里实例化好,直接调用验证结果。

expect(appController.getHello()).toBe('Hello World!')——简单粗暴,调一下看返回对不对。出厂质检,合格才能上柜。


九、一张图看懂 NestJS 的请求流程

一个 HTTP 请求从到达到返回,在 NestJS 里经历了什么:

HTTP 请求
  ↓
NestFactory.create(AppModule)  ← 工厂启动应用
  ↓
AppModule (根模块)              ← 总调度
  ↓
TodosModule (业务模块)          ← 路由匹配
  ↓
TodosController                 ← 前台接单:@Get / @Post / @Delete / @Put
  ↓ (自动依赖注入 TodosService)
TodosService                    ← 后厨干活:CRUD 业务逻辑
  ↓
NotFoundException (找不到时)    ← 标准化错误
  ↓
HTTP 响应 (JSON)

工厂造应用 → 模块分业务 → 控制器接请求 → 服务干逻辑 → 错误类兜底。每一层职责单一,层层递进。


十、设计模式不是玄学

今天最深的感受是——NestJS 把设计模式用成了基础设施。

  • 工厂模式NestFactory.create() 造应用,MixueFactory.create('ice') 造冰激凌。调用者不需要知道内部细节,工厂负责生产。
  • 装饰器模式@Controller@Get@Injectable 给类贴标签,不修改原有代码就叠加功能。
  • 依赖注入@Injectable() 声明可注入,构造函数声明需求,NestJS 自动装配。
  • 模块化@Module 把 Controller 和 Service 打包,业务隔离,按需加载。

readme 里说设计模式是"面向接口的编程,抽象的,23 种"。今天用了两种(工厂 + 装饰器),NestJS 里处处都是它们的影子。

设计模式不是面试时背的八股文,是工程化开发的肌肉记忆。


PS:前端有 Next.js,后端有 NestJS。一个是全栈瑞士军刀,一个是企业级蜜雪冰城。搭配使用,风味更佳。