写 Node.js 后端时,你是不是也曾遇到过这些糟心事儿?:
- 代码越写越乱,想找个用户逻辑得翻遍整个项目;
- 重复代码一堆,每个接口都要写日志、验证、异常处理;
- 协作时 teammate 写的“野生代码”,你看了直挠头……
别慌!NestJS 就是来“治”这些问题的~它把前端 Angular 的企业级开发模式搬到了后端,用“模块化+依赖注入+AOP”三大神器,直接把 Node.js 后端开发从“野路子”拉到“正规军”水平!今天咱们就用最接地气的话,把 NestJS 的核心概念掰碎了讲——保证你看完会说:“哦!原来这么简单~”
一、NestJS 核心设计哲学:为什么它能治“代码混乱”?🤔
1.1 架构理念:从 Angular 来的“治乱神器”
你知道吗?NestJS 的设计灵感其实来自前端的 Angular!没错~就是那个让前端代码井井有条的框架~它把 Angular 那套“企业级开发方法论”原封不动搬到了后端,比如模块化(把功能拆成小盒子)、依赖注入(不用自己 new 对象)、面向切面编程(AOP)(抽离重复逻辑)——直接让 Node.js 后端告别“写一段丢一段”的野生写法!
看!这就是 NestJS 最核心的 Module 装饰器,你的整个应用就靠它组织起来~:
// NestJS 的核心设计受到 Angular 的启发
// 采用模块化、依赖注入、面向切面编程等企业级模式
@Module({
imports: [
/* 其他模块 */
],
controllers: [
/* 控制器 */
],
providers: [
/* 服务提供商 */
],
exports: [
/* 导出服务 */
],
})
export class AppModule {}1.2 设计模式对比:NestJS vs Express/Koa
可能有人会问:“我用 Express/Koa 也能写后端,为啥要用 NestJS?”咱们用一张图对比下:
左边是 Express/Koa 的“自由派”——中间件堆一堆,回调嵌套容易“ callback hell ”,灵活性高但容易“写崩”;
右边是 NestJS 的“正规军”——模块化+依赖注入+AOP,直接把后端代码拉到企业级可维护性水平!
二、NestJS 核心概念:用“小盒子”组织你的代码 🌟
2.1 模块(Modules):后端代码的“收纳盒”
你有没有过这种经历?写项目时,想改个用户功能,结果翻遍routes、controllers、services文件夹,才找到对应的代码?NestJS 的 Module 就是来解决这个问题的!
模块是 NestJS 最基础的“组织单元”——把一个功能的**控制器(接请求)、服务(业务逻辑)、依赖(比如数据库)**全塞进一个“小盒子”里,每个盒子只干自己的事儿,互相之间明确依赖。比如下面这个UserModule:
// 用户模块示例
@Module({
imports: [DatabaseModule, AuthModule], // 依赖的其他模块(要连数据库+鉴权)
controllers: [UserController], // 处理用户请求的“接待员”
providers: [UserService, UserRepository], // 处理业务逻辑的“打工人”
exports: [UserService], // 分享给其他模块用的服务
})
export class UserModule {}举个例子:UserModule要处理用户相关的请求,所以依赖DatabaseModule(连数据库查用户)和AuthModule(验证用户身份);里面的UserController负责接/users的请求,UserService负责写业务逻辑——是不是像“搭积木”一样清晰?
模块的 4 大作用,记好啦:
- 🏗️ 代码组织:按功能拆分成小盒子,再也不用乱翻代码;
- 🔒 封装性:盒子里的实现对外隐藏,只暴露需要的服务;
- 🔗 依赖管理:明确模块之间的依赖关系,避免“隐式依赖”;
- 🎯 可测试性:每个模块独立,测试时不用启动整个应用。
2.2 控制器(Controllers):后端的“接待员”
控制器是 NestJS 里直接和用户请求打交道的角色——就像餐厅的“接待员”:用户说“我要一份番茄炒蛋”(发 GET 请求),接待员记下来,然后喊厨房(服务)做;用户说“我要加个蛋”(发 POST 请求),接待员再把需求传过去。
看下面这个UserController,它负责处理所有/users开头的请求:
// 用户控制器
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get() // 处理 GET /users 请求(查用户列表)
@HttpCode(200)
async findAll(@Query() query: PaginationQuery) {
return this.userService.findAll(query) // 把活儿派给UserService
}
@Get(':id') // 处理 GET /users/1 请求(查单个用户)
async findOne(@Param('id') id: string) {
return this.userService.findOne(+id) // 转成数字传给服务
}
@Post() // 处理 POST /users 请求(创建用户)
@UsePipes(ValidationPipe) // 用管道验证参数
async create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto)
}
}这里的装饰器超重要,咱们挑几个常用的解释:
@Controller('users'):给控制器加路由前缀,所有请求都得从/users进;@Get()/@Post()/@Put():对应 HTTP 方法,告诉控制器“这个方法处理哪种请求”;@Param()/@Query()/@Body():从请求里“抽”参数——比如@Param('id')就是从 URL 里拿id,@Body()就是拿 POST 请求的 body;@UsePipes():给这个接口加“参数验证管道”——比如CreateUserDto里的邮箱格式,不用自己写 if 判断,管道帮你搞定!
2.3 提供者(Providers):业务逻辑的“打工人”
控制器接了请求,得有人干“脏活累活”啊——比如查数据库、验证邮箱是否存在、发欢迎邮件,这些核心业务逻辑全交给**提供者(Providers)**干!
提供者是 NestJS 里最“能干”的角色——只要加个@Injectable()装饰器,它就能被依赖注入到其他组件里(比如控制器)。看下面这个UserService:
// 用户服务 - 业务逻辑层
@Injectable()
export class UserService {
constructor(
private readonly userRepository: UserRepository,
private readonly configService: ConfigService,
@Inject('EMAIL_SERVICE') private emailService: EmailService
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
// 1. 验证邮箱是否已存在(业务逻辑)
if (await this.userRepository.exists(createUserDto.email)) {
throw new ConflictException('Email already exists')
}
// 2. 创建用户并存数据库
const user = this.userRepository.create(createUserDto)
await this.userRepository.save(user)
// 3. 发欢迎邮件(调用其他服务)
await this.emailService.sendWelcomeEmail(user.email)
return user
}
}这段代码里有两个关键点:
- 依赖注入:
UserService需要UserRepository(查数据库)、ConfigService(读配置)、EMAIL_SERVICE(发邮件),不用自己new,NestJS 会自动“塞”进来; - 业务逻辑集中:所有和“创建用户”相关的逻辑(验证、存库、发邮件)都在这儿——控制器里只需要调用
this.userService.create(),不用写任何业务逻辑!
另外,提供者还能自定义哦~比如下面这个EmailServiceProvider,用provide当 key,后面可以随便换实现(测试时用 Mock,上线时用真的):
// 自定义提供者 - 值提供者
export const EmailServiceProvider = {
provide: 'EMAIL_SERVICE',
useValue: new MockEmailService(), // 可以是类、值、工厂等
}2.4 依赖注入(Dependency Injection):不用自己“new”对象!💡
终于讲到 NestJS 的核心机制了——依赖注入(DI)!这玩意儿说复杂也复杂,说简单也简单:不用自己创建对象,NestJS 会帮你把依赖的实例“送”到构造函数里~
比如你要喝奶茶,不用自己买原料、煮茶、做奶茶——直接点外卖,外卖小哥(NestJS)把奶茶(实例)送到你手里(构造函数)!
NestJS 支持三种注入方式,咱们挨个看:
// 1. 构造函数注入(最推荐!简单直观)
@Injectable()
export class UserService {
constructor(
private userRepository: UserRepository, // 自动注入UserRepository实例
@Optional() private logger?: Logger // 可选注入(没有的话就是undefined)
) {}
}
// 2. 属性注入(少用!不太直观)
@Injectable()
export class UserService {
@Inject(UserRepository)
private userRepository: UserRepository // 直接给属性注入
}
// 3. 自定义提供者(超灵活!值、类、工厂都能上)
@Module({
providers: [
UserService,
{
provide: AbstractRepository,
useClass: UserRepository, // 类提供者(用UserRepository实现AbstractRepository)
},
{
provide: 'CONFIG_OPTIONS',
useValue: { timeout: 5000 }, // 值提供者(直接传配置对象)
},
{
provide: 'DATABASE_CONNECTION',
useFactory: async (config: ConfigService) => {
return createConnection(config.get('DB_URL')) // 工厂提供者(异步创建连接)
},
inject: [ConfigService], // 注入ConfigService当参数
},
],
})
export class AppModule {}划重点:构造函数注入是最推荐的——代码清晰,容易测试;自定义提供者是最灵活的——比如数据库连接这种异步的,用工厂提供者完美解决!
三、NestJS 高级特性:用 AOP 告别“重复代码”!🛠️
你有没有过这种经历?每个接口都要写日志、验证、异常处理,重复代码一堆?NestJS 的AOP(面向切面编程)就是来解决这个问题的——把这些“横切逻辑”(日志、权限、验证、异常处理)从业务代码里抽出来,做成中间件、守卫、拦截器、管道,然后“贴”到需要的地方!
3.1 中间件(Middleware):请求的“必经之路”
中间件是请求到达控制器前的“拦截器”——所有请求都得先过中间件这一关!比如日志中间件(打请求日志)、认证中间件(验证 token)、跨域中间件(处理 CORS)。
看下面这个LoggerMiddleware,它会打印每个请求的时间、方法、URL:
// 日志中间件
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`)
next() // 一定要调用next(),不然请求会卡住!
}
}然后在模块里配置中间件,告诉 NestJS“哪些路由要用这个中间件”:
// 模块中配置中间件
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware) // 用LoggerMiddleware
.forRoutes('*') // 所有路由都要过这个中间件
.apply(AuthMiddleware) // 再用AuthMiddleware
.exclude('auth/login', 'auth/register') // 排除登录/注册接口
.forRoutes(UserController, ProductController) // 只给用户、商品控制器用
}
}小提醒:中间件里一定要调用next()——不然请求会“卡”在中间件里,永远到不了控制器!
3.2 守卫(Guards):接口的“门禁系统”
守卫是权限控制的“门神”——比如“只有管理员能访问/admin 接口”“只有登录用户能访问/user 接口”,这些逻辑全交给守卫干!
守卫要实现CanActivate接口,返回true(放行)或false(拦截)。看下面这个RolesGuard(角色守卫):
// 角色守卫
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 1. 从控制器/方法上拿需要的角色(比如@SetMetadata('roles', ['admin']))
const requiredRoles = this.reflector.get<string[]>(
'roles',
context.getHandler()
)
// 2. 没有需要的角色,直接放行
if (!requiredRoles) return true
// 3. 从请求里拿用户信息(比如AuthGuard注入的user)
const request = context.switchToHttp().getRequest()
const user = request.user
// 4. 检查用户角色是否符合要求
return requiredRoles.some((role) => user.roles?.includes(role))
}
}然后在控制器里用@UseGuards()装饰器“贴”上守卫,再用@SetMetadata()设需要的角色:
// 在控制器中使用
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard) // 先验证token(JwtAuthGuard),再检查角色(RolesGuard)
@SetMetadata('roles', ['admin']) // 需要admin角色
export class AdminController {
@Get('dashboard')
getDashboard() {
return 'Admin dashboard' // 只有admin能看到这个页面
}
}小技巧:守卫可以组合用——比如先验证 token(JwtAuthGuard),再检查角色(RolesGuard),两道门更安全!
3.3 拦截器(Interceptors):业务逻辑的“增强器”
拦截器是AOP 的核心——它能在方法执行前/后做一些操作,比如打日志、格式化响应、缓存结果。比如下面这三个常用拦截器:
① 日志拦截器:记录方法执行时间
// 日志拦截器
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest()
console.log(`Before: ${request.method} ${request.url}`) // 执行前打日志
const now = Date.now()
return next.handle().pipe(
tap(() => {
console.log(`After: ${Date.now() - now}ms`) // 执行后打时间差
})
)
}
}② 响应格式化拦截器:统一响应格式
// 响应格式化拦截器
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
code: 200,
message: 'Success',
data,
timestamp: new Date().toISOString(),
}))
)
}
}这个拦截器超实用!不管你后端返回什么数据,它都会统一包成{ code, message, data, timestamp }的格式——前端再也不用猜“这个接口返回的是对象还是数组”了!
③ 异常拦截器:统一异常处理
// 异常拦截器
@Injectable()
export class ExceptionInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
// 统一处理异常:取状态码和消息,再抛出去
const status = error.status || 500
const message = error.message || 'Internal server error'
throw new HttpException(message, status)
})
)
}
}划重点:拦截器的核心是RxJS 的流处理——你可以把请求-响应的过程想象成“水管里的水”,拦截器就是“水管上的阀门”:
next.handle()是“打开水管”,让数据流过去;pipe()是“接阀门”,可以在水流过的时候做各种操作(比如map转格式、tap打日志、catchError抓异常)。
3.4 管道(Pipes):数据的“安检员”🚰
管道是数据转换+验证的“双料专家”——比如:
- 把 URL 里的
id=“123”从字符串转成数字 123; - 验证
CreateUserDto里的邮箱是不是合法格式; - 检查密码是不是符合“至少 8 位+包含大小写”的要求。
简单说:管道就是“数据的守门员”,不合格的数据别想进业务逻辑!
看这个CustomValidationPipe(自定义验证管道),它做了两件事:
- 把 URL 里的
id转成数字; - 验证 DTO 的格式是否正确:
// 自定义验证管道
@Injectable()
export class CustomValidationPipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
// 1. 数据转换:把URL里的id从字符串转数字
if (metadata.type === 'param' && metadata.data === 'id') {
const id = parseInt(value, 10)
if (isNaN(id)) {
throw new BadRequestException('ID必须是数字哦~')
}
return id
}
// 2. DTO验证:用class-validator检查参数格式
if (metadata.metatype && this.isDto(metadata.metatype)) {
const errors = await validate(value)
if (errors.length > 0) {
throw new ValidationException(errors)
}
}
return value
}
private isDto(metatype: any): boolean {
const types = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}
}再看CreateUserDto——用class-validator的装饰器定义“数据规则”,管道会自动帮你检查:
// 在DTO中使用class-validator
export class CreateUserDto {
@IsString() // 必须是字符串
@MinLength(2) // 至少2个字符
@MaxLength(50) // 最多50个字符
name: string
@IsEmail() // 必须是合法邮箱格式
email: string
@IsStrongPassword() // 必须是强密码(比如包含大小写、数字、符号)
password: string
@IsOptional() // 可选参数
@IsEnum(UserRole) // 必须是UserRole枚举里的值(比如'user'或'admin')
role?: UserRole
}小技巧:class-validator+class-transformer是 NestJS 的“黄金组合”——前者验证数据格式,后者把数据转成 DTO 类的实例,不用自己手动赋值!
四、Nest AOP 切面编程:告别“重复代码地狱”!🔥
讲完中间件、守卫、拦截器、管道,你有没有发现:这些东西本质上都是**AOP(面向切面编程)**的落地实现?
什么是 AOP?用“奶茶店”比喻就懂了!
想象你开了家奶茶店,每天要做这些重复的事:
- 客人进来时,说“欢迎光临”(日志);
- 点单时,检查有没有优惠券(验证);
- 做奶茶时,加珍珠要称重量(转换);
- 客人走时,说“欢迎下次再来”(日志)。
如果每个客人来,你都要手动做一遍这些事,是不是累死?AOP 就是把这些“重复动作”抽成“标准流程”——比如:
- 把“欢迎光临”和“欢迎下次再来”抽成“迎宾流程”(拦截器);
- 把“检查优惠券”抽成“验证流程”(管道);
- 把“加珍珠称重量”抽成“配料流程”(转换器)。
这样,你只需要专注做“做奶茶”这个核心业务,其他重复动作交给“标准流程”自动完成!
Nest AOP 的优势:从“混乱”到“清爽”的蜕变
咱们用一张表格对比传统业务代码和Nest AOP 代码的区别:
| 关注点类型 | 传统代码的痛点 | Nest AOP 怎么解决? | 用什么实现? |
|---|---|---|---|
| 日志记录 | 每个接口都要写console.log | 抽成拦截器,贴到需要的地方 | 拦截器(LoggingInterceptor) |
| 权限控制 | 每个接口都要查user.roles | 抽成守卫,贴到控制器/方法上 | 守卫(RolesGuard) |
| 数据验证 | 每个接口都要写if(!email) | 抽成管道,自动验证 DTO | 管道(ValidationPipe) |
| 异常处理 | 每个接口都要try...catch | 抽成异常拦截器,统一处理 | 拦截器(ExceptionInterceptor) |
| 性能监控 | 每个方法都要记Date.now() | 抽成拦截器,自动算执行时间 | 拦截器(LoggingInterceptor) |
一句话总结 AOP 的优势:把“重复的横切逻辑”从业务代码里“抠”出来,让业务代码只专注于“核心逻辑”——比如UserService.create()只需要关心“创建用户”,不用管日志、验证、异常处理!
五、最后:NestJS 到底帮我们解决了什么?🤔
看到这里,你应该明白 NestJS 的核心价值了吧?它不是“另一个 Node.js 框架”,而是一套“企业级后端开发的方法论”:
- 用模块化把代码装进“小盒子”,再也不用乱翻文件;
- 用依赖注入不用自己 new 对象,降低代码耦合;
- 用AOP抽离重复逻辑,告别“复制粘贴”的日子;
- 用装饰器把配置和逻辑分离,代码更清晰。
对于新手来说,NestJS 可能有点“重”——要学模块化、依赖注入、AOP 这些概念,但一旦学会,你会发现:写后端再也不是“写一段丢一段”,而是“搭积木”一样组装代码!
对于团队来说,NestJS 的“企业级规范”能让协作更顺畅——不管是新人还是老鸟,都按同一套规则写代码,再也不用看“野生代码”挠头!
写在最后:给新手的 3 个小建议 🌟
- 先写“小模块”:比如先写个
UserModule,包含控制器、服务、DTO,熟悉模块化的流程; - 多用官方工具:Nest CLI(
nest new创建项目、nest generate生成模块/控制器/服务)能帮你省很多时间; - 吃透 AOP:中间件、守卫、拦截器、管道是 NestJS 的“灵魂”,多写几个例子试试(比如写个日志拦截器、验证管道),你会爱上这种“抽离重复逻辑”的感觉!
好啦~今天的 NestJS 核心概念就讲到这儿!其实 NestJS 的门槛不高,只要把“模块化+依赖注入+AOP”这三个概念搞懂,你就能写出“企业级”的 Node.js 后端代码~
如果这篇文章帮到了你,不妨点个赞告诉我~咱们下次再聊 NestJS 的进阶技巧:比如“如何用 NestJS 连数据库?”“如何写单元测试?”“如何部署 NestJS 应用?”不见不散哦~ 😘
- 本文链接:https://fridolph.top/posts/2025-09-01__nest01-re0
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。