跳转到主要内容

Nest 使用笔记

第一章:搭建并运行第一个 Nest 项目

装 Nest CLI、建项目、跑起开发服务看到 Hello World,再打开 learnhub 的真实 main.ts,看一个生产级 Nest 启动做了哪几件事——全局前缀、URI 版本化、参数校验、Swagger、日志。整门课的起点,也是后续每章要反复改的文件。

  • Nest
  • 入门

这一章是整门课的第一节,目标只有一个:从零搭出一个能跑的 Nest 服务。做完你会拥有一个本地运行的服务、自己加了一个带参数的接口,并且打开了贯穿全课程的真项目 learnhub 的入口文件 main.ts,看懂一个生产级启动到底做了哪几件事。

这一章你会做出什么

  • nest new 建项目、npm run start:dev 跑起来,看到 Hello World。
  • 自己加一个 /hello?name=小明 接口,返回 你好,小明!
  • 打开 learnhub 的 main.ts,看生产级启动的 5 件事:全局前缀、版本化、参数校验、Swagger、日志。

这一门课围绕一个真实项目 learnhub(一个学习社区后端)展开——它把后续每一章要讲的东西(数据库、登录、权限、缓存、搜索、实时通信、部署)都真实实现了。建议你现在就 clone 一份对照着看:

git clone <learnhub> learnhub
cd learnhub
cat src/main.ts   # 这一章第六步逐行讲它

前置条件:装好 Node.js(建议 20 LTS,learnhub 要求 node>=20,去 nodejs.org 下载)。终端能跑通这两条就说明环境就绪:

node -v   # 例如 v20.11.0
npm -v    # 例如 10.2.4

第一步:安装 Nest CLI

Nest 提供 @nestjs/cli,用来创建项目、生成代码、构建运行。全局装一下:

npm install -g @nestjs/cli
nest -v   # 看到版本号(如 11.x)就成功了

也可以不全局装,每次 npx @nestjs/cli ... 调用,但全局装更顺手。CLI 记得偶尔升一下:npm update -g @nestjs/cli,否则用它建出来的项目可能不是最新版。

第二步:创建项目

找个目录,执行:

nest new hello-nest

它会问用哪个包管理器(npm / pnpm / yarn),选 npm 回车,然后自动装依赖(一分钟上下)。生成的 hello-nest/ 结构:

hello-nest/
├── src/
│   ├── app.controller.ts       # 控制器:定义接口路由
│   ├── app.controller.spec.ts  # 控制器测试
│   ├── app.module.ts           # 根模块:把所有 controller/service/子模块组装起来
│   ├── app.service.ts          # 服务:写业务逻辑
│   └── main.ts                 # 入口:创建应用、启动监听
├── test/
├── package.json
├── tsconfig.json
└── nest-cli.json

先记住三个文件,后面整门课都在反复改它们:

  • main.ts:应用入口,启动服务、做全局配置(前缀、校验、文档)、监听端口。
  • app.module.ts:根模块,把所有 controller、service、子模块组装在一起。
  • app.controller.ts + app.service.ts:一个接口的最小形态——controller 接请求,service 写逻辑。

想跳过交互式提问,可以一步建好:nest new hello-nest -p npm --skip-git --skip-install,之后自己 cd 进去 npm install

第三步:跑起来,看到 Hello World

进项目目录,启动开发服务(带热重载,改代码自动重启):

cd hello-nest
npm run start:dev

看到这两行就说明起来了:

[Nest] LOG [NestFactory] Starting Nest application...
[Nest] LOG [NestApplication] Nest application successfully started

一张图理解 Nest 项目从命令到监听端口的启动链路:

flowchart TB A["nest new 创建项目"] --> B["npm run start:dev"] B --> C["执行 main.ts"] C --> D["NestFactory.create(AppModule)"] D --> E["装配 controllers 和 providers"] E --> F["app.listen(3000)"]

默认监听 3000 端口。浏览器打开 http://localhost:3000,页面显示 Hello World!。这个返回怎么来的?看 src/app.controller.ts

// hello-nest/src/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 {
    return this.appService.getHello(); // 返回值就是响应体
  }
}
  • @Controller() 把这个类标记为控制器。
  • @Get()getHello 绑定到根路径 GET /,返回值就是响应体。
  • 逻辑在 app.service.ts
// hello-nest/src/app.service.ts
import { Injectable } from "@nestjs/common";

@Injectable()
export class AppService {
  getHello(): string {
    return "Hello World!";
  }
}

AppService 通过构造函数注入到 AppController(这是 Nest 的依赖注入,第三章细讲,现在先当「能直接用」)。试着把 'Hello World!' 改成 '你好,Nest!',保存——start:dev 是热重载,不用手动重启,刷新立刻看到新内容。这就是 Nest 的开发循环:改代码 → 保存 → 自动重启 → 刷新看效果。

第四步:用 nest generate 快速加代码

Nest CLI 能按模板生成 controller / service / module,并自动注册到模块。生成一个 hello 模块:

nest generate module hello      # 生成 hello.module.ts,并自动加进 AppModule.imports
nest generate controller hello  # 生成 hello.controller.ts
nest generate service hello     # 生成 hello.service.ts

不想生成测试文件就加 --no-spec

第五步:加一个自己的接口

src/hello/hello.controller.ts,写一个接收 name 参数的 /hello 接口:

// hello-nest/src/hello/hello.controller.ts
import { Controller, Get, Query } from "@nestjs/common";
import { HelloService } from "./hello.service";

@Controller("hello")
export class HelloController {
  constructor(private readonly helloService: HelloService) {}

  @Get()
  getHello(@Query("name") name?: string): string {
    return this.helloService.greet(name);
  }
}
  • @Controller('hello'):这个控制器下所有接口路径都带 /hello 前缀。
  • @Get():对应 GET /hello
  • @Query('name'):从 URL query string 取 name/hello?name=小明),没传就是 undefined

逻辑放 service:

// hello-nest/src/hello/hello.service.ts
import { Injectable } from "@nestjs/common";

@Injectable()
export class HelloService {
  greet(name?: string): string {
    return name ? `你好,${name}!` : "你好,陌生人!";
  }
}

保存,访问:http://localhost:3000/hello你好,陌生人!http://localhost:3000/hello?name=小明你好,小明!

到这里,你已经会建项目、跑服务、加接口了。但这个 main.ts 只有一行 NestFactory.create + app.listen,是个玩具。真实项目的启动要做一堆全局配置——下面打开 learnhub 的 main.ts 看生产级启动长什么样。

第六步:打开 learnhub 的 main.ts——生产级启动做了哪几件事

前面 nest new 出来的 main.ts 是最小骨架。learnhub 作为一个真实在跑的项目,它的 main.tscreatelisten 之间还做了 5 件全局配置。这是后续每一章的基础,现在先认个脸,后面章节会逐个展开。

// learnhub/src/main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // ① 用 winston 替换内置 Logger(日志按天切割、分 info/error 文件)
  app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));

  // ② 全局前缀:所有路由变成 /api/xxx
  app.setGlobalPrefix("api");

  // ③ URI 版本化:/api/v1/xxx(第十章 Prisma 用 /api/v2/posts 做对照)
  app.enableVersioning({ type: VersioningType.URI, defaultVersion: "1" });

  // ④ 全局参数校验:transform 转对象、whitelist 去多余字段、forbidNonWhitelisted 直接报错
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
      forbidNonWhitelisted: true,
    }),
  );

  // ⑤ Swagger 接口文档:访问 http://localhost:3000/api/doc
  const config = new DocumentBuilder()
    .setTitle("learnhub API")
    .addBearerAuth() // Swagger 顶部 Authorize 输入 Bearer token
    .build();
  SwaggerModule.setup("api/doc", app, SwaggerModule.createDocument(app, config));

  await app.listen(process.env.PORT || 3000);
}
bootstrap();

逐条说为什么:

  • ① winston 日志:Nest 自带的 Logger 只往控制台打,生产上要按天切割、分文件、分级别存。learnhub 用 nest-winston 替换掉内置 Logger,全应用的日志统一走 winston。日志体系第十一章附近细讲。
  • ② 全局前缀 /api:所有路由自动加 /api 前缀(/posts/api/v1/posts)。好处是前后端分离时,前端统一配 /api 代理到后端,Nginx 也好按前缀转发,不会和前端静态路由撞。
  • ③ URI 版本化:接口带版本号 /api/v1/xxx。将来接口要大改又不忍心直接破坏老前端时,新开 /api/v2,老版本保留。learnhub 的 /api/v1/posts 用 TypeORM、/api/v2/posts 用 Prisma,就是靠版本号让两套实现并存的(第十章讲)。
  • ④ 全局 ValidationPipe:所有接口的 @Body() 参数自动按 DTO 校验——少传字段、类型不对、传了 DTO 里没声明的字段,直接被拦在进 Controller 之前。transform: true 还会把普通对象实例化成 DTO 类实例(这样 DTO 里的 get skip() 这类计算属性才生效)。第二章细讲。
  • ⑤ Swagger:根据你的 controller 和 DTO 自动生成接口文档,挂在 /api/doc,能在浏览器里直接试接口。第十四章细讲。

注意:这 5 件配置的顺序有时有讲究(比如全局前缀要在 Swagger 挂载之前设好,否则文档里的路径不带前缀)。现在先照抄 learnhub 的顺序,后面章节改的时候会回来讲为什么。

这一章的成果

  1. nest new 创建标准项目结构,npm run start:dev 的热重载开发循环。
  2. nest generate 生成模块 / 控制器 / 服务并自动注册到 AppModule
  3. controller 接请求(@Controller / @Get / @Query)、service 写逻辑的分层写法,自己写了一个带参数的 /hello 接口。
  4. 打开 learnhub 的 main.ts,认全了生产级启动的 5 件事(日志 / 全局前缀 / 版本化 / 参数校验 / Swagger),知道后续每一章在改什么。

常见问题

  • 端口被占用EADDRINUSE :3000):改 main.ts 里的 app.listen(3000) 换个端口,或设环境变量 PORT=3005(learnhub 就是 process.env.PORT || 3000)。
  • 改了代码没生效:确认用 npm run start:dev(带 --watch),不是 npm run start
  • nest 命令找不到:CLI 没全局装好,重跑 npm install -g @nestjs/cli,或临时用 npx @nestjs/cli ...
  • learnhub 起不来、报缺环境变量:learnhub 要连 MySQL/Redis 等,先 cd learnhub/docker && docker compose up -d mysql redis,再 cp .env.example .env。第一章只读它的代码不用全跑起来。

下一章讲 Nest 怎么接收请求的各种参数(路径参数、query、请求体、header),并用 ValidationPipe + DTO 做真正带校验的接口——对应 learnhub main.ts 里第 ④ 件事。