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 项目从命令到监听端口的启动链路:
默认监听 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.ts 在 create 和 listen 之间还做了 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 的顺序,后面章节改的时候会回来讲为什么。
这一章的成果
- 用
nest new创建标准项目结构,npm run start:dev的热重载开发循环。 - 用
nest generate生成模块 / 控制器 / 服务并自动注册到AppModule。 - controller 接请求(
@Controller/@Get/@Query)、service 写逻辑的分层写法,自己写了一个带参数的/hello接口。 - 打开 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 里第 ④ 件事。