一天学会nextjs

前端工程师极速入门 NestJS 后端 — 实战学习笔记(含全套习题答案)

前言(前端视角)

作为前端开发者学习 NestJS,无需深究后端底层原理、操作系统、服务器内核等复杂知识,核心目标是打通前后端开发链路,独立开发可用、规范、符合企业标准的后端接口

前端入门 NestJS 只需要掌握核心 6 大能力:

  • 基础 CRUD 接口编写
  • 精准接收前端各类请求参数
  • 统一接口响应格式,适配前端 Axios 拦截器
  • 生产级跨域配置
  • 第三方数据库/工具全局挂载
  • JWT 登录鉴权、接口权限控制

本文基于实战踩坑经验总结,贴合前端思维,0 后端基础可直接上手,学完可独立开发完整前后端项目。

一、NestJS 核心架构(前端必懂核心)

1. 核心分层架构(企业固定规范)

NestJS 最核心的执行链路:Controller(控制器)→ Service(业务层)

  • Controller 控制器:只负责「对接前端」,接收请求、解析参数、调用业务服务,不写任何业务逻辑
  • Service 业务服务:只负责「处理逻辑」,数据校验、数据库操作、业务计算、数据处理,不直接对接前端请求

前端口诀(必背):控制器只传话,服务层干实事,职责分离不混乱。

2. 依赖注入(前端最易理解的核心特性)

依赖注入是 NestJS 的核心设计,彻底告别手动实例化类,解决前端开发者初学后端最大的困惑。

核心代码示例:

typescript
constructor(private readonly authService: AuthService) {}

通俗白话解析:

  • 无需手动new AuthService()实例化
  • 只需给 Service 类添加@Injectable()装饰器
  • Nest 框架自动全局创建单例实例,全局唯一
  • 控制器直接通过this.authService.xxx()调用方法

前端类比理解:等同于全局挂载的公共工具类,项目启动自动初始化,任意页面/模块直接调用。

二、控制器全套语法(前端必会 100%)

1. @Controller 路由前缀配置

统一模块路由前缀,简化接口路径,是企业开发标准化写法。

typescript
import { Controller } from '@nestjs/common';

@Controller() // 无统一前缀,接口需写完整路径
@Controller('products') // 统一前缀 /products,当前模块所有接口自动拼接前缀

企业规范:99% 的业务模块必须配置统一路由前缀,方便接口管理、版本迭代。

2. 四大核心请求方式(对应前端所有请求场景)

完全对齐前端 Axios 请求方法,一一对应,无学习成本:

  • @Get():查询数据(列表、详情、分页数据)
  • @Post():新增数据、登录、提交表单
  • @Patch():局部更新数据(只修改部分字段)
  • @Delete():删除数据(单条/批量删除)

3. 前端参数接收(重中之重,对接前端核心)

前端所有传参方式,仅对应 4 种后端装饰器,全覆盖无遗漏:

① 路径参数 @Param(动态路由 /:id)

适用场景:获取单条详情、删除单条数据、修改单条数据

typescript
import { Get, Param, ParseIntPipe } from '@nestjs/common';

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.productService.findOne(id);
}

核心关键点:

  • URL 路径参数默认都是字符串类型,后端需要数字类型必须转换
  • ParseIntPipe 两大核心作用:自动字符串转数字、非法参数直接返回 400 错误,拦截无效请求

② 查询参数 @Query(URL 拼接 ?key=value)

适用场景:分页查询、条件筛选、关键词搜索、多参数筛选

typescript
import { Get, Query } from '@nestjs/common';
import { QueryDto } from './dto/query.dto';

@Get()
findAll(@Query() dto: QueryDto) {
return this.productService.findAll(dto);
}

③ 请求体参数 @Body(Post/Patch 表单参数)

适用场景:新增数据、批量修改、复杂表单提交

typescript
import { Post, Body } from '@nestjs/common';
import { CreateProductDto } from './dto/create-product.dto';

@Post()
create(@Body() dto: CreateProductDto) {
return this.productService.create(dto);
}

④ 完整请求对象 @Request()

适用场景:获取请求头、登录用户信息、Cookie 等原生请求数据

typescript
import { Request } from '@nestjs/common';

async login(@Request() req) {
return req.user; // 获取守卫解析后的登录用户信息
}

三、管道 Pipe 核心知识点(参数校验神器)

ParseIntPipe(前端开发高频使用)

是入门阶段最常用、必须掌握的内置管道,专门处理数字类型参数。

核心两大作用:

  1. 类型转换:自动将 URL 字符串参数转为 Number 数字类型
  1. 参数校验:参数为空、字母、特殊符号等非法格式,直接返回 400 客户端错误,不会进入业务逻辑

强制规范:所有 ID、页码、偏移量等数字参数,必须添加 ParseIntPipe 校验。

四、全局拦截器(统一返回格式,适配前端)

1. 核心作用

统一项目所有接口的响应结构,解决后端接口返回格式不统一、前端适配繁琐的问题,完美对接前端 Axios 全局拦截器。

2. 标准统一响应结构

json
{
"code": 200,
"data": {}, // 真实业务数据
"message": "success" // 提示信息
}

3. 前端核心收益

前端无需逐个适配接口返回格式,只需配置一次 Axios 拦截器,统一处理成功、失败状态,大幅减少重复代码。

五、跨域 CORS 原理(生产级配置)

项目采用白名单动态跨域方案,是企业生产环境标准写法,区别于开发环境的全局放行。

核心特性

  • 精准放行:仅允许配置的前端域名、IP 跨域请求
  • 测试兼容:Postman、Apifox 等无 Origin 的测试工具可直接放行
  • 安全拦截:陌生域名、非法请求直接拦截,杜绝跨域安全风险

优劣对比

app.enableCors():全局放行所有跨域请求,仅适合本地开发,线上生产环境禁止使用,存在安全隐患

六、全局模块 Global + 自定义依赖注入

以 ClickHouse 数据库模块为例,讲解第三方工具、数据库的全局挂载方案,适配所有全局工具。

1. @Global() 全局模块装饰器

被该装饰器标记的模块为全局模块,全项目所有模块无需手动导入,可直接注入使用

适用场景:数据库连接、日志工具、缓存、全局常量、公共工具方法。

2. 自定义注入令牌

第三方库、原生实例无法通过类注入,必须使用「字符串令牌 + @Inject」实现注入。

typescript
// 1. 定义注入令牌
provide: 'CLICKHOUSE_CLIENT'

// 2. 全局注入使用
import { Inject } from '@nestjs/common';
constructor(@Inject('CLICKHOUSE_CLIENT') private client) {}

七、登录鉴权守卫(AuthGuard)

守卫是 NestJS 权限控制核心,先校验、后执行接口逻辑,高效实现接口权限管控。

1. 两种核心守卫(全覆盖登录/权限场景)

  • AuthGuard('local'):本地账号密码守卫,仅用于登录接口,校验用户名、密码是否正确
  • AuthGuard('jwt'):令牌校验守卫,用于所有需要登录权限的接口,校验 Token 有效性

2. 守卫执行优先级

守卫执行顺序:请求进来 → 守卫校验 → 接口逻辑执行

Token 缺失、过期、错误、无效,直接返回 401 未授权错误,不会进入业务代码。

3. req.user 核心知识点

req.user 不是 Node 原生对象,是 JWT 守卫解析 Token 后,自动挂载到请求对象上的用户信息,可直接在接口中获取当前登录用户。

八、前端开发者专属必背总结

  1. Controller 只负责接参、转发请求,绝对不写业务逻辑
  1. Service 统一处理业务逻辑、数据操作、数据库查询
  1. 所有数字类型参数(ID、页码)必须搭配 ParseIntPipe 校验
  1. 全局工具/数据库采用 @Global 全局模块 + 自定义令牌注入
  1. 统一接口响应格式使用全局拦截器
  1. 接口权限、登录校验使用守卫 AuthGuard
  1. 生产环境必须使用白名单跨域,禁止全局跨域放行

九、企业级实战练习题(前端转后端专用)+ 完整标准答案

所有练习题均来自真实项目开发场景,完成后可直接写入简历,适配中小企业全栈开发需求。

练习1:规范商品模块接口(基础夯实)

需求

  • 给商品模块所有 @Param 路径参数(id)添加 ParseIntPipe 类型校验
  • 依托全局响应拦截器,统一所有 CRUD 接口返回格式
  • 配置接口权限:新增/修改/删除接口需 JWT 鉴权,查询接口公开访问

标准答案

typescript
import { Controller, Get, Post, Patch, Delete, Param, Body, Query, ParseIntPipe, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/jwt';
import { ProductService } from './product.service';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
import { QueryProductDto } from './dto/query-product.dto';

@Controller('products')
export class ProductController {
constructor(private readonly productService: ProductService) {}

// 公开接口
@Get()
findAll(@Query() dto: QueryProductDto) {
return this.productService.findAll(dto);
}

// 公开接口
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.productService.findOne(id);
}

// 需登录鉴权
@Post()
@UseGuards(AuthGuard('jwt'))
create(@Body() dto: CreateProductDto) {
return this.productService.create(dto);
}

// 需登录鉴权
@Patch(':id')
@UseGuards(AuthGuard('jwt'))
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateProductDto) {
return this.productService.update(id, dto);
}

// 需登录鉴权
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
remove(@Param('id', ParseIntPipe) id: number) {
return this.productService.remove(id);
}
}

练习2:生产级跨域配置改造

需求

  • 改造现有跨域配置,支持前端携带 Token、Cookie 跨域请求
  • 配置多环境白名单(本地开发、测试环境、线上生产环境域名)

标准答案(main.ts)

typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

// 多环境域名白名单
const allowOriginList = [
'http://localhost:5173',
'http://localhost:3000',
'https://test.xxx.com',
'https://www.xxx.com'
];

app.enableCors({
credentials: true, // 允许携带 Cookie、Token
origin: (origin, callback) => {
// 兼容 Postman/无Origin请求
if (!origin || allowOriginList.includes(origin)) {
callback(null, true);
} else {
callback(new Error('不允许跨域'));
}
}
});

await app.listen(3000);
}
bootstrap();

练习3:封装全局日志模块(高频面试考点)

需求:模仿 ClickHouse 全局模块写法,封装项目全局日志工具

  • 新建 LoggerModule 并设置为 @Global 全局模块
  • 封装日志方法(打印请求地址、请求方式、请求参数、请求时间)
  • 在商品 CRUD 接口中调用日志方法,记录每一次前端请求

标准答案

src/logger/logger.service.ts

typescript
import { Injectable } from '@nestjs/common';

@Injectable()
export class LoggerService {
requestLog(req: any) {
const logInfo = {
url: req.url,
method: req.method,
query: req.query,
body: req.body,
time: new Date().toLocaleString()
};
console.log('[请求日志]', logInfo);
}
}

src/logger/logger.module.ts(全局模块)

typescript
import { Global, Module } from '@nestjs/common';
import { LoggerService } from './logger.service';

@Global()
@Module({
providers: [LoggerService],
exports: [LoggerService]
})
export class LoggerModule {}

在商品控制器使用

typescript
constructor(
private readonly productService: ProductService,
private readonly logger: LoggerService
) {}

@Get()
findAll(@Query() dto: QueryProductDto, @Request() req) {
this.logger.requestLog(req);
return this.productService.findAll(dto);
}

练习4:精细化接口鉴权(真实项目权限逻辑)

需求:实现差异化权限控制,贴合后台管理系统真实场景

  • 登录接口、注册接口:无需 Token,公开访问
  • 商品列表、商品详情查询接口:无需 Token,公开访问
  • 商品新增、编辑、删除接口:必须携带有效 JWT Token,未登录直接拦截

标准答案(最终权限规则)

直接复用【练习1】代码即可实现:

  • 登录、注册、商品查询:无@UseGuards,公开访问
  • 新增/修改/删除:添加@UseGuards(AuthGuard('jwt'))强制鉴权

练习5:全局统一异常处理(项目必备优化)

需求:配合现有响应拦截器,完善项目异常处理机制

  • 创建全局异常过滤器,捕获项目所有未知错误、业务错误
  • 统一错误返回格式:code=500、message=错误描述、data=null
  • 区分参数错误、权限错误、服务器错误,返回对应提示

标准答案:全局异常过滤器 src/filter/http-exception.filter.ts

typescript
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();

let code = 500;
let message = '服务器异常';

if (exception instanceof HttpException) {
const status = exception.getStatus();
const errRes = exception.getResponse() as any;
code = status;
message = Array.isArray(errRes.message) ? errRes.message[0] : errRes.message;
}

response.status(200).json({
code,
message,
data: null
});
}
}

main.ts 全局注册

typescript
import { HttpExceptionFilter } from './filter/http-exception.filter';
app.useGlobalFilters(new HttpExceptionFilter());

练习6:前端适配型分页接口(Table 组件专用)

需求:改造商品列表接口,适配 Ant Design/Element Plus 表格组件分页需求

  • 接收前端传入的 page(当前页码)、pageSize(每页条数)参数
  • 后端实现分页查询逻辑,统计数据总数
  • 固定返回结构:{ page, pageSize, total, list }

标准答案

分页 DTO:query-product.dto.ts

typescript
import { IsOptional, IsInt, Min } from 'class-validator';
import { Transform } from 'class-transformer';

export class QueryProductDto {
@IsOptional()
@Transform(v => Number(v.value))
@IsInt()
@Min(1)
page: number = 1;

@IsOptional()
@Transform(v => Number(v.value))
@IsInt()
@Min(1)
pageSize: number = 10;
}

Service 分页逻辑

typescript
async findAll(dto: QueryProductDto) {
const { page, pageSize } = dto;
// 模拟数据库分页(真实项目替换为 ORM 分页)
const total = 86;
const list = [];

return {
page,
pageSize,
total,
list
};
}