当前位置: 首页 > news >正文

NestJS 入门(4):统一响应与异常处理

上一篇:NestJS 入门(3):Guard 如何挡住未登录请求? 讲了鉴权门槛。
业务代码里常见这样写:

thrownewUnauthorizedException('Invalid credentials');

但前端拿到的往往不是 Nest 默认的异常结构,而是统一信封,例如:

{"code":1001,"msg":"Invalid credentials","data":null}

成功时则是:

{"code":0,"msg":"success","data":{/* 业务数据 */}}

这篇文章只讲清楚一件事:

Interceptor 负责成功包装,Exception Filter 负责失败整形——两边约定同一套信封,前端才能稳定解析。


1. 为什么要统一信封?

如果每个接口自己返回:

{"ok":true,"result":...}{"success":1,"payload":...}{"error":"xxx"}

前端就要写一堆特殊判断。统一成:

字段含义
code业务错误码;0表示成功
msg给人看的说明
data成功时的业务数据;失败常为null

前端只需:

if(data.code===0){returndata.data;}// 否则按 data.code / data.msg 提示用户

HTTP 状态码仍然有用(401/403/404),但业务语义优先看code
例如同是 401,可以细分成「未登录 / token 无效 / 凭证错误」。


2. 成功路径:全局 Response Interceptor

Nest 启动时挂上全局拦截器:

asyncfunctionbootstrap(){constapp=awaitNestFactory.create(AppModule);app.useGlobalInterceptors(newResponseInterceptor());app.useGlobalFilters(newHttpExceptionFilter());awaitapp.listen(3000);}

拦截器大致是这样:

@Injectable()exportclassResponseInterceptorimplementsNestInterceptor{intercept(context:ExecutionContext,next:CallHandler):Observable<unknown>{constresponse=context.switchToHttp().getResponse();if(this.shouldSkip(response)){returnnext.handle();// SSE / 文件流不要包}returnnext.handle().pipe(map((data)=>({code:0,msg:'success',data:data??null,})));}privateshouldSkip(response:Record<string,unknown>):boolean{constcontentType=typeofresponse.getHeader==='function'?response.getHeader('Content-Type'):response.contentType;if(typeofcontentType==='string'){if(contentType.includes('text/event-stream'))returntrue;if(contentType.includes('application/octet-stream'))returntrue;}returnfalse;}}

Controller 仍然可以「直接 return 业务对象」:

@Get()findAll(){returnthis.projectsService.findAll(userId);// 实际响应会被包成 { code: 0, msg: 'success', data: [...] }}

你不用在每个方法里手写信封。

为什么 SSE 要跳过?

SSE 要持续写:

data: {"event":"content","data":"你好"}\n\n

如果也走成功拦截器,可能被一次性包成 JSON 信封,流就坏了。
所以看到text/event-stream(或文件下载)时直接next.handle(),不做map


3. 失败路径:全局 Exception Filter

业务里抛:

thrownewUnauthorizedException('Invalid credentials');

若没有过滤器,Nest 默认也会返回 JSON,但字段名、结构和成功信封往往不一致。
全局过滤器把所有异常收口成同一形状:

@Catch()exportclassHttpExceptionFilterimplementsExceptionFilter{catch(exception:unknown,host:ArgumentsHost){constctx=host.switchToHttp();constresponse=ctx.getResponse<Response>();letstatus=HttpStatus.INTERNAL_SERVER_ERROR;letmessage='Internal server error';if(exceptioninstanceofHttpException){status=exception.getStatus();constexceptionResponse=exception.getResponse();if(typeofexceptionResponse==='string'){message=exceptionResponse;}elseif(typeofexceptionResponse==='object'&&exceptionResponse!==null){message=((exceptionResponseasRecord<string,unknown>).messageasstring)||message;}}elseif(exceptioninstanceofError){// 也可把 JWT 相关 Error 映射成 401message=exception.message;}constcode=this.mapStatusToErrorCode(status);response.status(status).json({code,msg:message,data:null,});}privatemapStatusToErrorCode(status:number):number{switch(status){caseHttpStatus.UNAUTHORIZED:return1001;caseHttpStatus.FORBIDDEN:return1003;caseHttpStatus.NOT_FOUND:return1100;caseHttpStatus.BAD_REQUEST:return1503;caseHttpStatus.CONFLICT:return1102;caseHttpStatus.SERVICE_UNAVAILABLE:return1502;default:return1500;}}}

关键点:

  1. @Catch()不传参数 = 抓住所有异常(不只是HttpException
  2. 从异常里取出 HTTP status 与可读 message
  3. 映射成业务code
  4. 永远返回{ code, msg, data }

这样前端无论成功失败,解析路径都一样。


4. 请求链路对照

成功: Controller return data → ResponseInterceptor map 成 { code:0, msg:'success', data } → 前端拿到统一成功包 失败: Service throw UnauthorizedException('...') → 不走成功拦截器的 map(异常打断 Observable) → HttpExceptionFilter catch → { code:1001, msg:'...', data:null } → 前端拿到统一错误包

可以记成:

正常 return 走 Interceptor;抛异常走 Filter。
两边约定同一信封字段,前端只认这一套。


5. HTTP 状态码 vs 业务错误码

两者分工不同:

维度HTTP status业务code
给谁看网关、浏览器、通用客户端业务前端、运营排障
粒度粗(401/404/500)细(1001/1100/1503…)
例子401 Unauthorized1001 InvalidToken / 1004 InvalidCredentials

常见分段(示例):

区间含义
0成功
1000–1099认证鉴权
1100–1199项目相关
1200–1299文档相关
1500+系统 / 校验类

Filter 里用mapStatusToErrorCode做「粗映射」够入门;
更精细时,可以在抛异常时直接带业务码(自定义异常类),Filter 优先读业务码。


6. 业务代码怎么写才干净?

Service:抛语义清晰的异常

asynclogin(email:string,password:string){constuser=this.findUserByEmail(email);if(!user){thrownewUnauthorizedException('Invalid credentials');}// ...}

不要在 Service 里手动拼:

return{code:1001,msg:'...',data:null};// 不推荐:和拦截器职责打架

Controller:继续薄

@Post('login')login(@Body()body:{email:string;password:string}){returnthis.authService.login(body.email,body.password);}

成功自动包;失败自动整形。

前端:按信封解包

http.interceptors.response.use((response)=>{constdata=response.data;if(data&&typeofdata==='object'&&'code'indata){if(data.code===0){returndata.data;// 业务层只看到真正的 data}returnPromise.reject(newError(data.msg||'请求失败'));}returnresponse;});

7. 自定义异常信息时注意getResponse()形态

UnauthorizedException('Invalid credentials')时,getResponse()可能是字符串,也可能是:

{"statusCode":401,"message":"Invalid credentials","error":"Unauthorized"}

所以 Filter 里要同时处理stringobject,否则msg可能变成[object Object]或拿不到可读文案。

校验类异常(如 ValidationPipe)的message还可能是字符串数组,进阶时可以再归一成一句或列表。


8. 和 Guard / JWT 的关系

Guard 鉴权失败时,底层同样会抛出 HTTP 异常(常见 401)。
只要全局 Filter 在,Guard 挡下的请求也会变成统一错误包,而不是「有的接口结构不一样」。

这正是系列串起来的好处:

  1. Module / Controller / Service 分层
  2. DI 接线
  3. Guard 守门
  4. Interceptor + Filter 统一出口

前端感知到的 API,始终是同一套语言。


9. 小结

  • 统一信封:{ code, msg, data },成功code === 0
  • ResponseInterceptor:包装成功返回;SSE/文件流要跳过
  • ExceptionFilter:把HttpException/ 普通Error收口成同一错误包
  • 业务层优先throw new UnauthorizedException(...),不要手写两套返回结构
  • HTTP status 表达传输层语义,业务code表达产品语义

对照前几篇,可以再多一句:

  1. 哪个 Controller 接请求?
  2. 哪个 Service 做业务?
  3. 哪个 Module 组装?
  4. 依赖从哪注入?
  5. 有没有 Guard?
  6. 成功谁包装、失败谁整形?前端拿到的信封长什么样?

下一篇会讲:Pipe 与 DTO 校验——为什么@Body()进来的脏数据,可以在进 Controller 之前就被拦下。

系列导航

  • 上一篇:NestJS 入门(3):Guard 如何挡住未登录请求?
  • 第二篇:NestJS 入门(2):依赖注入到底解决了什么问题?
  • 第一篇:NestJS 入门(1):先搞懂 Module、Controller、Service
http://www.jsqmd.com/news/1375113/

相关文章:

  • 2026年程序员必看:掌握大模型,收藏这份AI时代进阶指南!
  • MiniLPA:革命性eSIM管理桌面解决方案,一站式解决数字身份配置难题
  • 奔赴世界之巅|德达西藏团队护航珠峰,开启高原巡检第一站
  • 2026连云港楼顶漏水避坑指南 - 企业资讯
  • Java 业务异常体系设计
  • 2026呼和浩特楼顶漏水避坑指南 - 企业资讯
  • 2026百色楼顶漏水避坑指南 - 房屋修缮
  • 从入门到精通:nov.el高级功能与快捷键全攻略
  • 2026承德楼顶漏水避坑指南,正规公司,质保可查 - 防水百科
  • Catppuccin for Alacritty:如何让你的终端界面既美观又高效?
  • 强化学习训练稳定性三大归一化技术:OpenAI Baselines深度解析与工程实践
  • 探索前沿AI工具集:开发者必备的智能API资源库
  • 3步完成Qwen3-14B部署:从模型下载到高性能推理实战
  • Kimsuky AI钓鱼+GitHub C2+AsyncRAT攻击拆解与全网检测防御实战
  • 宾馆设备回收公司哪家专业 - 产品推荐官
  • 2026年高端金银线批发商选购全指南:品控标准、交付效率与选型参考 - 贰拾壹度
  • 2026年|杭州富阳区GEO服务商代理加盟怎么选?本地靠谱推荐与合作启动指南 - 小随科技
  • verilog HDLBits刷题[Bulid a circuit from a simulation waveform]“Sim/circuit6”---Combinational circuit6
  • 2026烟台楼顶漏水避坑指南 - 管道一点通
  • Loop:终极免费的macOS窗口管理神器,让桌面工作流效率翻倍
  • 南京市高淳区GEO服务商代理加盟本地靠谱推荐:综合首选极义GEO,附选型指南与避坑提醒 - 企业新闻快传
  • Waydroid项目中TikTok黑屏问题的深度分析与容器化Android图形渲染解决方案
  • 角质层:透皮吸收的主要屏障及其突破策略
  • KMS-Tools-Portable便携版:终极软件激活工具完整使用指南
  • Boss Show Time:终极招聘时间可视化插件,精准把握求职黄金时机
  • Milvus集群攻防实战:认证绕过漏洞检测、复现与生产加固配置清单
  • 组件库预算有限时,先守住哪些发布环节
  • 3个技巧让Android投屏到电脑变得如此简单:scrcpy完全指南
  • 如何在LightGBM中启用GPU加速:快速提升机器学习训练性能的完整指南
  • 深度解析MiniMax-H3-TAE:高效视觉解码器的技术原理与实践指南