NestJS 入门:用"模块化 + 装饰器"读懂大型后端框架

一、NestJS 是什么

NestJS 是目前最流行的 Node.js 大型后端框架之一。它的核心思想:用面向对象 + 装饰器,把后端代码组织得规整、可扩展

为什么需要它?看一个对比:

  • 原生 Node/Express 写后端:一个文件几千行,路由、校验、业务、数据库操作全混在一起,项目一大人就懵;
  • NestJS 写后端:强制分层------模块(Module)、控制器(Controller)、服务(Service)各司其职,像搭积木一样清晰。

本质上,NestJS 是经典的 MVC 模式

  • M(Model):数据模型,数据库抽象
  • C(Controller):控制器,接收前端请求
  • V(View):视图层,返回给前端的响应

前后端分离时代,V 交给了前端,后端专注 M 和 C,中间由 Service 承担业务逻辑。

二、项目骨架:从 main.ts 开始

先看入口文件 main.ts

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  // 工厂模式:实例化一个 Nest 后端应用
  const app = await NestFactory.create(AppModule);
  // 启动 Web 服务,默认 3000 端口
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

要点:

  1. 工厂模式NestFactory.create(AppModule) 不是 new 一个对象,而是由"工厂"根据模块配置帮你组装出整个应用------你不需要关心内部细节;
  2. 按需加载 :NestJS 是模块化架构,AppModule 是根模块,应用启动时由它开始装配所有功能;
  3. 用 JS 类比:NestFactory.create 就像一个"App 组装车间",你给它一张说明书(Module),它按说明书把控制器、服务、路由全部接好线。

三、Module:整个应用的"说明书"

app.module.ts

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],      // 服务:复杂业务 CRUD
})
export class AppModule {}

Module 是 NestJS 的最小组织单元,它声明了三类东西:

配置项 含义 JS 类比
imports 引入其他模块 引入子模块组件
controllers 路由入口,校验、简短逻辑 前台接待员
providers 可注入的服务,复杂业务 后台干活的员工

理解模块化的价值:一个大型项目 = 很多个 Module 拼起来。比如你的项目有 TodosModule(待办)、UserModule(用户)、OrderModule(订单),每个模块独立开发、独立维护,最后在根模块里组装。

flowchart TD App[AppModule 根模块] -->|imports| T[TodosModule] App -->|controllers| C[AppController] App -->|providers| S[AppService] T --> TC[TodosController] T --> TS[TodosService]

四、Controller:请求的"前台接待"

Todos.controller.ts(省略装饰器 import 部分):

ts 复制代码
@Controller('todos')          // 路由前缀:所有 /todos 开头的请求
export class TodosController {
  constructor(private readonly todosService: TodosService) {}

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

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

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

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

  @Delete(':id')              // DELETE /todos/1
  remove(@Param('id') id: string): { message: string } {
    this.todosService.remove(Number(id));
    return { message: '删除成功' };
  }
}

Controller 的关键认知:

  1. 装饰器即路由@Controller('todos') 定前缀,@Get/@Post/@Put/@Delete 定方法------不用手写 app.get('/todos', ...)
  2. 参数装饰器@Param('id') 取路径参数,@Body() 取请求体------类似 Express 的 req.params.idreq.body,但更声明式;
  3. Controller 不干重活:它只做"接请求、传参数、返回结果",真正的业务逻辑委托给 Service。

用 JS 类比装饰器:装饰器就像给函数"贴标签"。@Get('todos') 相当于在普通函数上贴了一张"我是 GET /todos 的处理函数"的标签,框架看到标签就把路由绑定好。这比手写 router.get() 更直观、更模块化。

五、Service:业务逻辑的"员工"

Todos.service.ts

ts 复制代码
@Injectable()               // 可被依赖注入
export class TodosService {
  // 模拟内存数据库
  private todos: Todo[] = [
    { id: 1, weight: 1, completed: false },
    { id: 2, weight: 2, completed: false },
  ];

  findAll(): Todo[] {
    return this.todos;
  }

  findOne(id: number): Todo {
    const todo = this.todos.find((t) => t.id === id);
    // 容错:找不到就抛异常,而不是返回 undefined
    if (!todo) throw new NotFoundException(`todo ${id} not found`);
    return todo;
  }

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

  remove(id: number): void {
    const index = this.todos.findIndex((t) => t.id === id);
    if (index === -1) throw new NotFoundException(`todo ${id} 不存在`);
    this.todos.splice(index, 1);
  }

  update(id: number, patch: Partial<Todo>): Todo {
    const todo = this.findOne(id);
    Object.assign(todo, patch);   // 把更新字段合并进原对象
    return todo;
  }
}

Service 的职责:处理真正的业务(查数据、改数据、校验存在性),并对 Controller 负责。三个细节值得学习:

  1. @Injectable() 装饰器:标记这个类可以被 NestJS 的依赖注入(DI)容器自动创建并注入到需要它的地方;
  2. NotFoundException :业务严谨稳定靠的是容错模块------找不到资源时抛明确异常,而不是返回 undefined 让上层摸不着头脑;
  3. Partial<Todo> :TS 内置工具类型,表示"Todo 的部分字段",更新时只传需要改的字段。用 JS 类比就是:{...todo, ...patch} 合并对象。

六、依赖注入(DI):Controller 怎么拿到 Service

你可能好奇:TodosController 里根本没有 new TodosService(),它为什么能用?

ts 复制代码
constructor(private readonly todosService: TodosService) {}

这是 依赖注入(Dependency Injection) :NestJS 的容器看到 Controller 的构造函数需要 TodosService,就会自动创建实例并"塞"进来。

用 JS 类比:

js 复制代码
// 不使用 DI:手动创建依赖(耦合)
class TodosController {
  constructor() {
    this.todosService = new TodosService(); // 自己 new,换实现要改代码
  }
}

// 使用 DI:声明"我需要什么",框架给什么(解耦)
class TodosController {
  constructor(todosService) {
    this.todosService = todosService; // 谁传的?NestJS 容器
  }
}

DI 的好处:

  • 解耦:Controller 不关心 Service 怎么创建,只关心它有什么能力;
  • 易测试:测试时传一个 mock 的 service 进去即可(后面测试小节会看到);
  • 易替换:换数据库实现,只改 Service,Controller 一行不动。

七、数据流:一次请求的完整旅程

把整个 Todos 模块串起来,一次 GET /todos 的旅程:

sequenceDiagram participant B as 浏览器 participant C as TodosController participant S as TodosService B->>C: GET /todos Note over C: @Get() 命中路由 C->>S: findAll() S-->>C: 返回 Todo[] C-->>B: JSON 响应

前端只跟 Controller 打交道,Controller 只跟 Service 打交道,Service 管数据。 这就是分层架构的威力------每一层职责单一,改动互不牵连。

八、测试:NestJS 内置的测试体系

app.controller.spec.ts

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!');
    });
  });
});

测试的要点:

  1. Test.createTestingModule:不用启动整个服务器,只组装被测的 Controller + Service,跑得快;
  2. app.get<AppController>(AppController):从测试容器里取出被测对象;
  3. describe / it / expect:Jest 测试框架的三大件,描述测试分组、用例、断言。

这就是 DI 带来的测试红利:想测 Controller,就只给它配好 Service,其余一律不加载

九、完整项目结构回顾

把上传的 8 个文件归位,就是 NestJS 的标准项目骨架:

flowchart LR main.ts --> AppModule AppModule --> AppController --> AppService AppModule --> TodosModule --> TodosController --> TodosService TodosService --> NotFoundException app.controller.spec.ts -.测试.-> AppController
文件 角色 一句话职责
main.ts 入口 工厂模式创建应用并启动
app.module.ts 根模块 组装所有模块/控制器/服务
app.controller.ts 根控制器 处理 / 请求
app.service.ts 根服务 返回首页文案
Todos.module.ts 功能模块 声明 Todos 的控制器和服务
Todos.controller.ts 功能控制器 /todos 的 CRUD 路由
Todos.service.ts 功能服务 内存数据 + CRUD 业务
app.controller.spec.ts 测试 验证 Controller 行为

十、结语

NestJS 学起来看似装饰器满天飞,但底层逻辑非常朴素:

  1. 模块化:Module 组织一切,大项目 = 模块拼装;
  2. 分层:Controller 接请求 → Service 做业务,各司其职;
  3. 装饰器:用声明式语法替代手写路由;
  4. 依赖注入:框架自动组装依赖,解耦且易测。

如果你熟悉 Express,会惊讶于"同样的功能,代码怎么突然这么规整";这正是 NestJS 的价值------用结构化的框架约束,让后端项目从"能跑"走向"能长期维护"

相关推荐
天道kabuto1 天前
前端每日知识点:Next.js 渲染与缓存:从 Pages Router 到 Cache Components 的范式迁移
next.js
名字还没想好☜3 天前
React 实现暗黑模式切换:localStorage 持久化、SSR 首屏闪烁与跟随系统主题
前端·javascript·react.js·ecmascript·react·next.js
Linguwen4 天前
AI外贸建站04|三个账号第一次联动:让Codex写一个网页,从生成到上线走完整条流水线
next.js·外贸独立站·企业官网·外贸建站·ai建站
Linguwen4 天前
AI外贸建站03|建站三件套:GitHub、Next.js、Codex各干什么,为什么别再用模板建站
next.js·外贸独立站·企业官网·外贸建站·ai建站
古夕6 天前
my-first-ai-web_学习记录05——NextAuth Adapter 存储用户信息
typescript·全栈·next.js
10年前端老司机6 天前
别卷CRUD了!前端用Next.js+LangChain.js,低成本冲进AI高薪赛道
前端·langchain·next.js
为你学会写情书8 天前
用 Redis 当数据库?Next.js 笔记系统的数据层设计
next.js
为你学会写情书8 天前
Next.js 16 全栈实战:从零搭一个 Markdown 笔记系统
next.js
Asize10 天前
框架的说明书是写给 AI 看的:我用 Next.js 搭了个博客
人工智能·代码规范·next.js