NestJS快速上手
一、为什么是 NestJS
NestJS 是一个企业级后端框架,用 JavaScript 编写,完全支持并推荐使用 TypeScript。它在底层并不自己造 HTTP 轮子,而是封装了两种成熟的 Node 框架:
| 适配器 | 特点 | 是否默认 |
|---|---|---|
Express | 生态最广、中间件最丰富、心智模型熟悉 | ✅ 默认 |
Fastify | 性能更高、内置 schema 序列化 | ❌ 需手动切换 |
相比「一把梭」的 Express 项目,NestJS 提供了几个非常关键的能力:
- 依赖注入容器:服务之间靠类型「接线」,而不是到处
require。 - 模块化组织:每个业务域是一个独立的
Module,边界清晰、可测试、可拆分。 - AOP 能力:中间件、守卫、管道、拦截器、过滤器五层切面,横切逻辑不入侵业务代码。
- 约定优于配置:分层职责明确(Controller 收请求、Service 写业务、Repository 管数据)。
一句话总结:它把 Express 的灵活性,装进了 Spring 式的工程化外壳里。
所以想真正用好它,需要先了解两个前置概念:
- IOC 控制反转与依赖注入
- 装饰器的语法与元数据机制
这两个概念不必等完全掌握再动手,边写边体会是更高效的路径。
二、环境准备:安装与创建项目
2.1 全局安装脚手架
NestJS 的官方脚手架是 @nestjs/cli:
bashnpm install -g @nestjs/cli # 验证安装 nest --version
如果
npm全局安装不便,也可以用pnpm add -g @nestjs/cli或npx @nestjs/cli new project_name。
2.2 创建项目
bash# 非严格模式(tsconfig 的 strict 全开为 false,新手友好) nest new project_name # 严格模式(推荐用于正式项目) nest new project_name --strict
注意:
--strict是nest new的参数,写在项目名后面。
执行后 CLI 会交互式询问使用哪个包管理器(npm / pnpm / yarn),推荐选择 pnpm:安装快、磁盘占用小、依赖隔离更严格。
2.3 创建后发生了什么
脚手架会自动生成一套能直接运行的骨架:
textproject_name/ ├── src/ │ ├── app.controller.ts # 根控制器 │ ├── app.controller.spec.ts # 单元测试 │ ├── app.module.ts # 根模块 │ ├── app.service.ts # 根服务 │ └── main.ts # 应用入口 ├── test/ # e2e 测试 ├── package.json ├── tsconfig.json └── nest-cli.json
目录该长成什么样、企业级项目如何分层,参见《目录结构》。
三、先跑起来:启动项目
bashcd project_name # 一次性启动(不监听文件变化) pnpm run start # 开发模式:监听文件变化自动重启(实际开发用这个) pnpm run start:dev # 生产模式:先构建再运行编译产物 pnpm run build && pnpm run start:prod
默认监听 3000 端口,打开 http://localhost:3000 会看到:
textHello World!
3.1 理解 main.ts
main.ts 是整个应用的引导入口,后面几乎所有的全局配置都会写在这里:
ts// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT ?? 3000); } bootstrap();
这里 NestFactory.create(AppModule) 做了一件很重要的事:它会根据 AppModule 的依赖关系,实例化整张对象图——这正是指依赖注入在背后工作的地方。
以上命令的区别:
| 命令 | 作用 | 场景 |
|---|---|---|
pnpm run start | ts-node 编译执行一次 | 快速验证 |
pnpm run start:dev | watch 模式,改代码自动重启 | 日常开发 |
pnpm run start:prod | 运行 dist/ 下的 JS 产物 | 部署 |
pnpm run start:debug | watch + 调试端口 | 断点调试 |
四、请求生命周期:Nest 的执行顺序
这是理解 NestJS 的总纲。一个 HTTP 请求从进入应用到返回响应,依次穿过下面这些环节:
text请求 │ ├─ 1. 中间件 Middleware → 全局 → 控制器 ├─ 2. 守卫 Guards → 全局 → 控制器 → 路由 ├─ 3. 拦截器 Interceptors → 前置逻辑(全局 → 控制器 → 路由) ├─ 4. 管道 Pipes → 全局 → 控制器 → 路由(参数校验/转换) ├─ 5. 路由处理器 Handler → Controller 方法 → Service ├─ 6. 拦截器 Interceptors → 后置逻辑(路由 → 控制器 → 全局) ├─ 7. 异常过滤器 Filters → 捕获以上任意环节抛出的异常 │ 响应
记住三条关键规则,绝大多数「为什么我的代码没执行」的问题都能自己解出来:
- 中间件在最外层,它甚至不知道路由参数长什么样。
- 守卫先于管道,所以「未登录」应该早于「参数不合法」被拦截。
- 拦截器是「包住」处理器的,因此能做耗时统计、统一响应包装。
守卫、管道、拦截器、过滤器各自有独立详解,后文都会给出导览链接。
五、核心概念速览
简单来说,NestJS 是基于 MVC 思路设计的框架:用控制器接请求、提供器写业务、模块做组织、中间件处理横切逻辑。一切能力都通过装饰器声明式地挂载到类和路由上。
| 概念 | 关键词 | 负责什么 | 一句话类比 |
|---|---|---|---|
| Controller | @Controller | 解析请求、返回响应 | 餐厅服务员 |
| Provider / Service | @Injectable | 业务逻辑、可被注入 | 后厨 |
| Module | @Module | 组织、封装、导出 | 门店 |
| Middleware | NestMiddleware | 请求前置处理 | 大门口保安 |
| Pipe | PipeTransform | 参数转换与校验 | 食材质检 |
| Guard | CanActivate | 认证授权 | 检票口 |
| Interceptor | NestInterceptor | 前后置增强 | 中间件式「包装纸」 |
| Filter | ExceptionFilter | 统一异常输出 | 售后处理 |
后面几节按「你写代码时最常碰到的顺序」逐一展开。
六、工程化配置:环境变量
真实项目几乎不可能只有一套配置:开发连本地库、生产连线上库。NestJS 官方推荐配合 @nestjs/config 来做。
6.1 安装依赖
bash# @nestjs/config 是运行时依赖,不加 -D pnpm add @nestjs/config # cross-env 用于跨平台设置环境变量,仅开发期需要 pnpm add -D cross-env
原文这里漏了
@nestjs/config,且cross-env的-D位置写成了add cross-env -D,两者都能装但不够规范。
6.2 配置 npm scripts
在 package.json 中把三个脚本改成带 NODE_ENV 的版本:
json{ "scripts": { "start": "cross-env NODE_ENV=development nest start", "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:prod": "cross-env NODE_ENV=production node dist/main" } }
这样开发环境与生产环境会被自动打上不同的 NODE_ENV,后续就能据此加载不同的 .env 文件。
cross-env的作用是解决 Windows 与 Unix 在KEY=value cmd语法上的差异,避免「在 macOS 上能跑、在 Windows 上炸掉」。补充:
start:prod不经过nestCLI,所以需要先用pnpm run build生成dist/,并确保入口是dist/main.js(脚手架默认即是)。
6.3 创建环境变量文件
.env.development
envNODE_ENV=development PORT=3000 APP_URL=localhost DATABASE_URL=localhost DATABASE_USERNAME=root DATABASE_PASSWORD=jeff123456 DATABASE_NAME=my_store
.env.production
envNODE_ENV=production PORT=4000 APP_URL=localhost DATABASE_URL=localhost DATABASE_USERNAME=root DATABASE_PASSWORD=change_me DATABASE_NAME=my_store
.env(可选,用于放所有环境共用的配置)
envAPP_NAME=nest-demo
⚠️ 务必把
.env*加入.gitignore,改为提交一份.env.example供团队参考。把数据库密码提交进仓库是最常见的事故之一。
6.4 在根模块中引入
ts// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ProductsModule } from './products/products.module'; @Module({ imports: [ ConfigModule.forRoot({ // 后面的文件优先级更高,可以覆盖前面的默认值 envFilePath: ['.env', `.env.${process.env.NODE_ENV || 'development'}`], isGlobal: true, ignoreEnvFile: false, }), ProductsModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
三个参数的含义:
| 参数 | 作用 |
|---|---|
envFilePath | 数组靠后的优先级更高,因此 .env.development 会覆盖 .env 中的同名项 |
isGlobal | 设为 true 后,其他模块无需再 imports: [ConfigModule] 即可注入 ConfigService |
ignoreEnvFile | 设为 true 则完全忽略文件、只读进程环境变量(容器化部署时常用) |
6.5 推荐:补齐 env.d.ts 类型定义
环境变量是字符串运行时值,编辑器无法推断其存在与类型。给 ProcessEnv 补上声明后,写代码时会有自动补全、拼错也能立刻报错。
在 types/env.d.ts 中创建:
ts/** * 扩展 NodeJS 的 ProcessEnv 接口,用于定义项目中使用的环境变量类型。 */ declare namespace NodeJS { export interface ProcessEnv { /** 运行环境:development / production / test */ NODE_ENV: 'development' | 'production' | 'test'; /** 应用名称 */ APP_NAME: string; /** 应用访问地址 */ APP_URL: string; /** 监听端口,运行时为字符串 */ PORT: string; /** 数据库连接地址 */ DATABASE_URL: string; /** 数据库用户名 */ DATABASE_USERNAME: string; /** 数据库密码 */ DATABASE_PASSWORD: string; /** 数据库名称 */ DATABASE_NAME: string; /** 连接池最小连接数,可选 */ DB_POOL_MIN?: string; /** 连接池最大连接数,可选 */ DB_POOL_MAX?: string; /** JWT 签名密钥 */ JWT_SECRET: string; /** JWT 过期时间,如 '1h'、'7d' */ JWT_EXPIRES_IN: string; /** Redis 主机地址 */ REDIS_HOST: string; /** Redis 端口,运行时为字符串 */ REDIS_PORT: string; /** 其他自定义环境变量 */ [key: string]: string | undefined; } }
确认 tsconfig.json 的 include 覆盖了 types 目录(默认的 "src/**/*" 不包含它时,需改成 "include": ["src/**/*", "types/**/*.d.ts"]),否则声明不生效。
6.6 在业务里安全地读取配置
ConfigService 返回值是 string | undefined,直接拼到连接串里很容易出现 undefined 混进 URL 的问题。生产项目建议在启动时做一次校验或不变量兜底:
ts// src/config/env.ts import { ConfigService } from '@nestjs/config'; export const getRequired = (config: ConfigService, key: string): string => { const value = config.get<string>(key); if (!value) throw new Error(`缺少必需的环境变量: ${key}`); return value; };
个人十分推荐把环境变量的类型声明做全。编程这件事,写到最后的都是耐心和哲学——类型就是替未来的自己写下的注释。
七、请求处理全解
7.1 控制器:请求的入口
控制器对应 MVC 中的 C(Controller)。在纯 Express 项目里,通常用 router + router_handler 分别处理路由与逻辑;NestJS 里则由 Controller 负责路由与参数解析,Service 负责业务。控制器不要写业务逻辑,也不应该直接操作数据库。
💡 快速生成一个带 CRUD 骨架的资源模块:
bashnest g resource products注意命令是
resource(不是resouce),它会同时生成 module / controller / service / dto,并询问是否生成 CRUD 模板。
路由
tsimport { Controller, Get } from '@nestjs/common'; import { ProductsService } from './products.service'; @Controller('products') // 路由前缀 /products export class ProductsController { constructor(private readonly productsService: ProductsService) {} @Get() // GET /products getProducts() { return this.productsService.getAll(); } }
访问 http://localhost:3000/products 即可拿到返回内容。方法返回的对象/数组会被自动序列化为 JSON,无需手动 res.json()。
请求对象:参数装饰器速查
| 装饰器 | 匹配对象或属性值 |
|---|---|
@Request(), @Req() | req |
@Response(), @Res() | res |
@Next() | next |
@Session() | req.session |
@Param(key?: string) | req.params / req.params[key] |
@Body(key?: string) | req.body / req.body[key] |
@Query(key?: string) | req.query / req.query[key] |
@Headers(name?: string) | req.headers / req.headers[name] |
@Ip() | req.ip |
@HostParam() | req.hosts |
⚠️ 一旦注入
@Res(),你就接管了整个响应流程,必须自己调用res.send()/res.json(),否则请求会一直挂起。仅在确实需要操作底层对象(比如res.redirect())时才使用。
响应头
要指定自定义响应头,使用 @Header() 装饰器或库特定的响应对象:
tsimport { Post, Header } from '@nestjs/common'; @Post() @Header('Cache-Control', 'no-store') create() { return 'This action adds a new product'; }
提示:从
@nestjs/common包中导入Header。
重定向
有两种实现方式:官方装饰器,或调用 express 的底层对象。
- 装饰器实现(推荐,返回值可控)
tsimport { Get, Redirect } from '@nestjs/common'; @Get('redirect-baidu') @Redirect('https://www.baidu.com', 301) redirectBaidu() { // 返回 { url, statusCode } 可以动态覆盖装饰器里的值 }
- Express 实现(会接管响应)
tsimport { Get, Res } from '@nestjs/common'; import type { Response } from 'express'; @Get('redirect') redirect(@Res() res: Response) { return res.redirect('https://docs.nestjs.com'); }
路由参数
ts// 方式一:拿整个 params 对象 @Get(':id') findOne(@Param() params: Record<string, string>) { console.log(params.id); return `This action returns a #${params.id} product`; } // 方式二:直接取指定参数(推荐,类型更明确) @Get(':id') findOne(@Param('id') id: string) { return `This action returns a #${id} product`; }
URL 参数永远是字符串。需要数字时不要手写
Number(id),而是交给管道:@Param('id', ParseIntPipe) id: number。
子域路由
如果希望某个控制器只响应指定域名的请求,可以在 @Controller 中配置 host:
ts@Controller({ host: 'admin.example.com' }) export class AdminController { @Get() index(): string { return 'Admin page'; } }
本地调试可通过
curl -H "Host: admin.example.com" http://localhost:3000/验证。
异步返回支持
处理器可以返回 Promise,Nest 会自动等待其 resolve 后再序列化,因此异步查询数据库非常自然:
ts@Get() async findAll(): Promise<Product[]> { return this.productsService.findAll(); }
同理也支持 Observable(配合 RxJS)。
请求负载:DTO
处理请求体时,应先定义数据传输对象(DTO),再用 @Body() 接收:
ts// src/products/dto/create-product.dto.ts export class CreateProductDto { name: string; price: number; description?: string; }
ts@Post() async create(@Body() createProductDto: CreateProductDto) { return this.productsService.create(createProductDto); }
⚠️ 重点:仅声明
CreateProductDto类型不会做任何校验——TypeScript 类型在编译后就被擦除了。要真正校验,必须配合ValidationPipe与class-validator,见 10.3 节。
查询参数
ts@Get() async findAll( @Query('page') page: number = 1, @Query('keyword') keyword?: string, ) { return `第 ${page} 页,关键词:${keyword ?? '(无)'}`; }
同样地,
page编译后仍是字符串,真实项目请用@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number。
7.2 提供器:依赖注入的载体
提供程序(Provider)是 Nest 的核心概念。服务、仓储、工厂、助手等几乎所有类都可以作为提供者。它的关键价值在于可以被依赖注入,从而让对象之间形成清晰的依赖关系——而「接线」这件事由 Nest 运行时负责。
定义服务
tsimport { Injectable } from '@nestjs/common'; import { Product } from './interfaces/product.interface'; @Injectable() export class ProductsService { private readonly products: Product[] = []; create(product: Product) { this.products.push(product); return product; } findAll(): Product[] { return this.products; } }
提示:使用 CLI 创建服务:
nest g service products
注册服务到模块
提供器必须注册到某个模块,才能被依赖注入使用:
ts// src/products/products.module.ts import { Module } from '@nestjs/common'; import { ProductsController } from './products.controller'; import { ProductsService } from './products.service'; @Module({ controllers: [ProductsController], providers: [ProductsService], exports: [ProductsService], // 需要被其他模块注入时导出 }) export class ProductsModule {}
使用服务
ts@Get(':id') getProduct(@Param('id', ParseIntPipe) id: number) { return this.productsService.findById(id); }
作用域提示
默认提供器是单例(singleton),整个应用共享一个实例。这正是你可以用 private readonly products: Product[] 在内存里「假装」数据库的原因——不过也意味着它不适合存用户会话。需要按请求隔离时使用 scope: Scope.REQUEST。
7.3 模块
模块是更有组织性的文件,通过 @Module() 装饰器导出。该装饰器提供四个常用参数:imports、exports、providers、controllers。子模块在根模块的 imports 中引入后,即在项目中可见。
一个 NestJS 项目必须有一个根模块(通常是
AppModule)。为了项目健壮性,通常会按功能与用途拆成多个模块,每个模块保持一定的组织性。
定义
tsimport { Module } from '@nestjs/common'; import { ProductsController } from './products.controller'; import { ProductsService } from './products.service'; @Module({ providers: [ProductsService], controllers: [ProductsController], }) export class ProductsModule {}
引入
tsimport { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ProductsModule } from './products/products.module'; @Module({ imports: [ ConfigModule.forRoot({ envFilePath: ['.env', `.env.${process.env.NODE_ENV || 'development'}`], isGlobal: true, ignoreEnvFile: false, }), ProductsModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
⚠️ 注入失败时最常见的两个原因:忘记在
providers注册,或跨模块使用时没在exports导出。看到Nest can't resolve dependencies of ...先查这两处。
7.4 中间件
中间件是在路由处理器之前调用的函数,可以访问 request、response 以及 next()。
类中间件
通过实现 NestMiddleware 接口的 use 方法来定义:
tsimport { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; @Injectable() export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { console.log(`[${req.method}] ${req.originalUrl}`); next(); // 千万别忘了调用,否则请求会挂起 } }
应用中间件
中间件不能写在 @Module 装饰器里,而是要让模块实现 NestModule 接口、在 configure 中注册:
tsimport { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { LoggerMiddleware } from './common/middleware/logger.middleware'; import { ProductsModule } from './products/products.module'; @Module({ imports: [ProductsModule], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware) .forRoutes('products'); // 也可以传 ProductsController 或 { path, method } } }
函数中间件
复用性不强的中间件,直接写函数即可,无需实现类:
tsimport { Request, Response, NextFunction } from 'express'; export function logger(req: Request, res: Response, next: NextFunction) { console.log('Request...'); next(); }
多个中间件
tsconsumer.apply(cors(), helmet(), logger).forRoutes(ProductsController);
中间件按
apply的顺序依次执行。
排除路由
tsconsumer .apply(LoggerMiddleware) .exclude({ path: 'products/health', method: RequestMethod.GET }) .forRoutes(ProductsController);
全局中间件
需要在 main.ts 中通过 app.use() 注册(此时拿到的是底层 express 实例):
ts// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { logger } from './common/middleware/logger.middleware'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(logger); await app.listen(process.env.PORT ?? 3000); } bootstrap();
区别:
configure()中的中间件受模块作用域约束;app.use()是全局的、且无法注入依赖(因为它不是 Nest Provider)。
八、异常处理
Nest 内置了一个异常层,负责处理应用中所有未处理的异常。当业务代码抛出异常而没人接住时,这一层会捕获它,并自动返回用户友好的响应。
标准情况下,抛出 HttpException 后会向客户端返回一个 JSON 对象:
json{ "statusCode": 400, "message": "Bad Request" }
8.1 抛出异常
tsimport { BadRequestException, NotFoundException } from '@nestjs/common'; findById(id: number) { const product = this.products.find((p) => p.id === id); if (!product) { throw new NotFoundException(`商品 ${id} 不存在`); } return product; }
Nest 提供了一组开箱即用的异常类(BadRequestException、UnauthorizedException、ForbiddenException、NotFoundException、ConflictException 等),优先使用它们,而不是手动 new HttpException(msg, 400)。
8.2 未处理异常的行为
如果抛出的是普通 Error(而非 HttpException),Nest 会返回:
json{ "statusCode": 500, "message": "Internal server error" }
这样设计是为了不把内部堆栈泄露给客户端。但真实项目里往往需要统一响应结构、埋入 traceId,这时就需要自定义异常过滤器。
8.3 自定义过滤器
ts// src/common/filters/http-exception.filter.ts import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, } from '@nestjs/common'; import type { Response } from 'express'; @Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const res = ctx.getResponse<Response>(); const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const message = exception instanceof HttpException ? exception.getResponse() : 'Internal server error'; res.status(status).json({ code: status, message, timestamp: new Date().toISOString(), path: ctx.getRequest().url, }); } }
注册为全局过滤器:
ts// src/main.ts app.useGlobalFilters(new AllExceptionsFilter());
@Catch()不带参数表示捕获所有异常。如果只想处理业务异常,写@Catch(HttpException)。更详细的过滤器用法(内置异常、全局过滤器、日志记录)见《过滤器》。
九、模块的进阶用法
9.1 共享模块
如果 ProductsService 需要在多个模块中使用,可以在模块中 exports 后,由使用方 imports:
ts@Module({ providers: [ProductsService], exports: [ProductsService], }) export class ProductsModule {}
9.2 全局模块
不想在每个模块里重复 imports,可以加上 @Global():
tsimport { Global, Module } from '@nestjs/common'; @Global() @Module({ providers: [ConfigService], exports: [ConfigService], }) export class SharedModule {}
@Global()会降低模块的显式性,只建议用于基础设施类模块(配置、日志、缓存)。
9.3 动态模块
需要根据运行参数生成模块时(如数据库连接),使用 register / forRoot 模式:
ts@Module({}) export class DatabaseModule { static forRoot(options: DbOptions): DynamicModule { return { module: DatabaseModule, providers: [{ provide: 'DB_OPTIONS', useValue: options }], exports: ['DB_OPTIONS'], }; } }
你已经在用的
ConfigModule.forRoot()、MongooseModule.forRoot()都是这一模式。
9.4 自定义提供器
除了直接写类名,providers 还支持 useClass / useValue / useFactory / useExisting 四种写法,用于替换实现、注入配置、按需创建:
ts@Module({ providers: [ ProductsService, { provide: 'CACHE_TTL', useValue: 60 }, { provide: 'REDIS', useFactory: (config: ConfigService) => createRedis(config), inject: [ConfigService], }, ], }) export class ProductsModule {}
完整原理见《IOC控制反转和依赖注入》。
十、实战:一个规范的商品 CRUD 接口
前面都是零件,这一节把它们组装成能跑的东西。
10.1 生成模块骨架
bashnest g resource products --no-spec
会生成:
textsrc/products/ ├── dto/ │ ├── create-product.dto.ts │ └── update-product.dto.ts ├── products.controller.ts ├── products.service.ts └── products.module.ts
10.2 数据模型与 DTO
ts// src/products/interfaces/product.interface.ts export interface Product { id: number; name: string; price: number; description?: string; }
ts// src/products/dto/create-product.dto.ts import { IsNumber, IsOptional, IsString, Min } from 'class-validator'; export class CreateProductDto { @IsString() name: string; @IsNumber() @Min(0) price: number; @IsOptional() @IsString() description?: string; }
ts// src/products/dto/update-product.dto.ts import { PartialType } from '@nestjs/mapped-types'; import { CreateProductDto } from './create-product.dto'; export class UpdateProductDto extends PartialType(CreateProductDto) {}
PartialType来自@nestjs/mapped-types;较新的 CLI 生成时一般已自动安装,如果报找不到模块就执行pnpm add @nestjs/mapped-types。它会把父类所有属性变成可选,避免重复声明。
10.3 DTO 校验与转换
bashpnpm add class-validator class-transformer
在 main.ts 中全局启用管道:
ts// src/main.ts import { ValidationPipe } from '@nestjs/common'; app.useGlobalPipes( new ValidationPipe({ whitelist: true, // 剥离 DTO 未声明的字段 forbidNonWhitelisted: true, // 出现多余字段直接报 400 transform: true, // 自动做类型转换 }), );
现在请求 POST /products 传入 { "name": 123 },会得到清晰的 400 响应:
json{ "statusCode": 400, "message": ["name must be a string"], "error": "Bad Request" }
常用校验装饰器速查见《class-validator备查表》,管道原理见《管道》。
10.4 服务层
ts// src/products/products.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; import { Product } from './interfaces/product.interface'; @Injectable() export class ProductsService { private products: Product[] = []; private nextId = 1; create(dto: CreateProductDto): Product { const product: Product = { id: this.nextId++, ...dto }; this.products.push(product); return product; } findAll(): Product[] { return this.products; } findOne(id: number): Product { const product = this.products.find((p) => p.id === id); if (!product) throw new NotFoundException(`商品 ${id} 不存在`); return product; } update(id: number, dto: UpdateProductDto): Product { const product = this.findOne(id); Object.assign(product, dto); return product; } remove(id: number): void { const index = this.products.findIndex((p) => p.id === id); if (index === -1) throw new NotFoundException(`商品 ${id} 不存在`); this.products.splice(index, 1); } }
10.5 控制器
ts// src/products/products.controller.ts import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Patch, Post, } from '@nestjs/common'; import { ProductsService } from './products.service'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; @Controller('products') export class ProductsController { constructor(private readonly productsService: ProductsService) {} @Post() create(@Body() dto: CreateProductDto) { return this.productsService.create(dto); } @Get() findAll() { return this.productsService.findAll(); } // 注意:静态路由要写在 ':id' 之前 @Get('health') health() { return { status: 'ok' }; } @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { return this.productsService.findOne(id); } @Patch(':id') update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateProductDto) { return this.productsService.update(id, dto); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) // DELETE 默认返回 200,改为 204 remove(@Param('id', ParseIntPipe) id: number) { return this.productsService.remove(id); } }
10.6 验证效果
bash# 新增 curl -X POST http://localhost:3000/products \ -H "Content-Type: application/json" \ -d '{"name":"键盘","price":299}' # 查询列表 curl http://localhost:3000/products # 参数不合法 → 400 curl -X POST http://localhost:3000/products \ -H "Content-Type: application/json" \ -d '{"name":"键盘","price":"abc"}' # id 不是数字 → 400(ParseIntPipe 拦截) curl http://localhost:3000/products/abc
到这里,一个具备参数校验、类型转换、统一错误输出的 CRUD 就闭环了。把它换成真实的数据库只需改 Service 层——这正是分层带来的好处。
十一、常见坑与最佳实践
11.1 高频踩坑清单
| 症状 | 原因 | 解决 |
|---|---|---|
Nest can't resolve dependencies of X | 提供器没注册 / 跨模块没导出 | 检查 providers 与 exports |
| 请求一直 pending 不返回 | 注入了 @Res() 却没调用 res.send();或中间件忘了 next() | 改用返回值方式,或补上响应调用 |
| DTO 校验完全不生效 | 没全局启用 ValidationPipe;或 DTO 未用 class-validator 装饰器 | app.useGlobalPipes(new ValidationPipe(...)) |
访问 /products/health 却进了 findOne | 路由顺序问题,:id 在前面 | 静态路由写在动态路由之前 |
@Query('page') 拿到的是字符串 | URL 参数永远是字符串 | ParseIntPipe / transform: true |
| 环境变量读不到 | 用了 process.env 但 .env 没加载;或 NODE_ENV 未设置 | 走 ConfigService,确认 envFilePath |
生产环境 start:prod 报找不到模块 | 没执行 pnpm run build,或入口路径不是 dist/main | 先 build,核对产物路径 |
| 启动报端口占用 | 上次 watch 进程没退出 | 结束占用 3000 的进程或改用其他端口 |
11.2 分层职责
- Controller:只做「解析请求 → 调用 Service → 返回结果」,不写业务判断、不直接操作数据库。
- Service:承载全部业务逻辑,保持无状态,便于单测。
- Module:按业务域切分,一个模块只对外暴露必要的接口(
exports)。
11.3 全局配置集中在 main.ts
建议在 main.ts 中一次性完成:全局管道、全局过滤器、全局前缀、CORS、Swagger、优雅关闭。
tsconst app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); // 所有路由加 /api 前缀 app.enableCors(); // 允许跨域 app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); app.enableShutdownHooks(); // 容器化部署时优雅退出
11.4 安全与配置
.env*绝不入库,用.env.example交接。- 生产环境关闭
synchronize(如果使用 TypeORM),改用 migration。 - 密钥类配置(
JWT_SECRET)不设默认值,缺失时启动即失败,而不是运行时才炸。
11.5 测试
脚手架自带的 *.spec.ts 用 Test.createTestingModule 做单测;test/ 目录用 supertest 做 e2e。Service 层是整个测试策略的重心,因为它是纯逻辑且不依赖 HTTP。
十二、可选:接口文档与测试
12.1 用 Swagger 自动生成接口文档
bashpnpm add @nestjs/swagger swagger-ui-express
ts// src/main.ts import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; const config = new DocumentBuilder() .setTitle('Nest 演示 API') .setVersion('1.0') .addBearerAuth() .build(); SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, config));
访问 http://localhost:3000/docs 即可在线调试。配合 DTO 上的 @ApiProperty(),接口文档会随代码自动更新。
12.2 用 Postman / curl 调试
不装 Swagger 也能测。除了上面的 curl,还可以把 Swagger 生成的 OpenAPI JSON(默认在 /docs-json)直接导入 Postman,自动生成整套请求。
十三、小结与下一步
回顾一下这篇覆盖了什么:
- ✅ 脚手架的安装、建项目、启动三件套
- ✅ 请求生命周期与五层切面的执行顺序
- ✅ Controller / Provider / Module / Middleware 四大核心抽象
- ✅ 多环境变量配置与类型声明
- ✅ 异常处理与自定义全局过滤器
- ✅ 一个带校验、类型转换、规范分层的 CRUD 实战
- ✅ 高频坑点清单