-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path4238683975.html
More file actions
793 lines (476 loc) · 45.2 KB
/
Copy path4238683975.html
File metadata and controls
793 lines (476 loc) · 45.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
<!DOCTYPE html>
<html lang="en" data-default-color-scheme="auto">
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" sizes="76x76" href="/img/fluid.png">
<link rel="icon" href="/img/fluid.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="theme-color" content="#2f4154">
<meta name="author" content="FangZhou">
<meta name="keywords" content="Vue,React,Nodejs,blogs,frontend,web,developer,programmer">
<meta name="description" content="AI全栈时代,如何优雅处理API响应?本文直击NestJS中手动拼接响应、DTO冗余与敏感数据泄露痛点。利用类型映射与序列化机制,践行DRY原则实现自动化数据脱敏,结合真实项目代码,提高工程化开发质量">
<meta property="og:type" content="article">
<meta property="og:title" content="NestJS全栈实战笔记:序列化与更优雅的响应结果处理">
<meta property="og:url" content="http://arkpln.github.io/4238683975.html">
<meta property="og:site_name" content="Ark的技术博客">
<meta property="og:description" content="AI全栈时代,如何优雅处理API响应?本文直击NestJS中手动拼接响应、DTO冗余与敏感数据泄露痛点。利用类型映射与序列化机制,践行DRY原则实现自动化数据脱敏,结合真实项目代码,提高工程化开发质量">
<meta property="og:locale" content="en_US">
<meta property="og:image" content="http://arkpln.github.io/images/nest-logo.png">
<meta property="article:published_time" content="2026-02-21T13:16:45.355Z">
<meta property="article:modified_time" content="2026-02-21T13:23:51.137Z">
<meta property="article:author" content="FangZhou">
<meta property="article:tag" content="nodejs">
<meta property="article:tag" content="backend">
<meta property="article:tag" content="nestjs">
<meta property="article:tag" content="fullstack">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="http://arkpln.github.io/images/nest-logo.png">
<title>NestJS全栈实战笔记:序列化与更优雅的响应结果处理 - Ark的技术博客</title>
<link rel="stylesheet" href="https://lib.baomitu.com/twitter-bootstrap/4.6.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://lib.baomitu.com/github-markdown-css/4.0.0/github-markdown.min.css">
<link rel="stylesheet" href="https://lib.baomitu.com/hint.css/2.7.0/hint.min.css">
<link rel="stylesheet" href="https://lib.baomitu.com/fancybox/3.5.7/jquery.fancybox.min.css">
<!-- 主题依赖的图标库,不要自行修改 -->
<!-- Do not modify the link that theme dependent icons -->
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_1749284_5i9bdhy70f8.css">
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_1736178_k526ubmyhba.css">
<link rel="stylesheet" href="/css/main.css">
<link id="highlight-css" rel="stylesheet" href="/css/highlight.css">
<link id="highlight-css-dark" rel="stylesheet" href="/css/highlight-dark.css">
<script id="fluid-configs">
var Fluid = window.Fluid || {};
Fluid.ctx = Object.assign({}, Fluid.ctx)
var CONFIG = {"hostname":"arkpln.github.io","root":"/","version":"1.9.8","typing":{"enable":true,"typeSpeed":70,"cursorChar":"_","loop":false,"scope":[]},"anchorjs":{"enable":true,"element":"h1,h2,h3,h4,h5,h6","placement":"left","visible":"hover","icon":""},"progressbar":{"enable":true,"height_px":3,"color":"#29d","options":{"showSpinner":false,"trickleSpeed":100}},"code_language":{"enable":true,"default":"TEXT"},"copy_btn":true,"image_caption":{"enable":true},"image_zoom":{"enable":true,"img_url_replace":["",""]},"toc":{"enable":true,"placement":"right","headingSelector":"h1,h2,h3,h4,h5,h6","collapseDepth":0},"lazyload":{"enable":true,"loading_img":"/img/loading.gif","onlypost":false,"offset_factor":2},"web_analytics":{"enable":true,"follow_dnt":true,"baidu":null,"google":{"measurement_id":null},"tencent":{"sid":null,"cid":null},"leancloud":{"app_id":"mEwkD7geCX7RWNO8srhDE2oP-gzGzoHsz","app_key":"4EF76hyiVA1QILHZThVbfoat","server_url":"https://mewkd7ge.lc-cn-n1-shared.com","path":"window.location.pathname","ignore_local":false},"umami":{"src":null,"website_id":null,"domains":null,"start_time":"2024-01-01T00:00:00.000Z","token":null,"api_server":null}},"search_path":"/local-search.xml","include_content_in_search":true};
if (CONFIG.web_analytics.follow_dnt) {
var dntVal = navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
Fluid.ctx.dnt = dntVal && (dntVal.startsWith('1') || dntVal.startsWith('yes') || dntVal.startsWith('on'));
}
</script>
<script src="/js/utils.js"></script>
<script src="/js/color-schema.js"></script>
<meta name="google-site-verification" content="5KfTZvtlydmBIzY97rVo-caP5jljAfmD3Qs3sBzY2Go"><style>ins.adsbygoogle[data-ad-status="unfilled"] { display: none !important; }</style>
<meta name="generator" content="Hexo 7.3.0"></head>
<body>
<header>
<div class="header-inner" style="height: 70vh;">
<nav id="navbar" class="navbar fixed-top navbar-expand-lg navbar-dark scrolling-navbar">
<div class="container">
<a class="navbar-brand" href="/">
<strong>Ark的技术博客</strong>
</a>
<button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<div class="animated-icon"><span></span><span></span><span></span></div>
</button>
<!-- Collapsible content -->
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto text-center">
<li class="nav-item">
<a class="nav-link" href="/" target="_self">
<i class="iconfont icon-home-fill"></i>
<span>快乐老家</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/archives/" target="_self">
<i class="iconfont icon-archive-fill"></i>
<span>笔岚档案</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/categories/" target="_self">
<i class="iconfont icon-category-fill"></i>
<span>文以类聚</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/tags/" target="_self">
<i class="iconfont icon-tags-fill"></i>
<span>按标签阅览</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about/" target="_self">
<i class="iconfont icon-user-fill"></i>
<span>联系我</span>
</a>
</li>
<li class="nav-item" id="search-btn">
<a class="nav-link" target="_self" href="javascript:;" data-toggle="modal" data-target="#modalSearch" aria-label="Search">
<i class="iconfont icon-search"></i>
</a>
</li>
<li class="nav-item" id="color-toggle-btn">
<a class="nav-link" target="_self" href="javascript:;" aria-label="Color Toggle">
<i class="iconfont icon-dark" id="color-toggle-icon"></i>
</a>
</li>
</ul>
</div>
</div>
</nav>
<div id="banner" class="banner" parallax="true" style="background: url('/images/nest-logo.png') no-repeat center center; background-size: cover;">
<div class="full-bg-img">
<div class="mask flex-center" style="background-color: rgba(0, 0, 0, 0.3)">
<div class="banner-text text-center fade-in-up">
<div class="h2">
<span id="subtitle" data-typed-text="NestJS全栈实战笔记:序列化与更优雅的响应结果处理"></span>
</div>
<div class="mt-3">
<span class="post-meta">
<i class="iconfont icon-date-fill" aria-hidden="true"></i>
<time datetime="2026-02-21 21:16" pubdate>
February 21, 2026 pm
</time>
</span>
</div>
<div class="mt-1">
<span class="post-meta mr-2">
<i class="iconfont icon-chart"></i>
5.8k words
</span>
<span class="post-meta mr-2">
<i class="iconfont icon-clock-fill"></i>
49 mins
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<div class="container-fluid nopadding-x">
<div class="row nomargin-x">
<div class="side-col d-none d-lg-block col-lg-2">
<aside class="sidebar d-none d-xl-block" style="margin-right:-1rem;z-index:-1"><ins class="adsbygoogle" style="display:flex;justify-content:center;min-width:160px;max-width:300px;width:100%;height:600px;position:sticky;top:2rem" data-ad-client="ca-pub-3240358922862141" data-ad-slot="6522434837"></ins><script> (adsbygoogle = window.adsbygoogle || []).push({}); </script></aside>
</div>
<div class="col-lg-8 nopadding-x-md">
<div class="container nopadding-x-md" id="board-ctn">
<div id="board">
<article class="post-content mx-auto">
<h1 id="seo-header">NestJS全栈实战笔记:序列化与更优雅的响应结果处理</h1>
<div class="markdown-body">
<h1 id="NestJS全栈实战笔记:序列化与更优雅的响应结果处理"><a href="#NestJS全栈实战笔记:序列化与更优雅的响应结果处理" class="headerlink" title="NestJS全栈实战笔记:序列化与更优雅的响应结果处理"></a>NestJS全栈实战笔记:序列化与更优雅的响应结果处理</h1><p>在利用AI完成Nestjs全栈开发中,一个常见但容易被<strong>0后端经验开发者</strong>忽视的问题是:<strong>如何优雅地控制 HTTP 响应体结构,避免手动拼装导致重复、出错以及难以维护?</strong></p>
<p>传统做法通常是在<code>service</code>层或 <code>controller</code> 层手动构造返回对象,例如创建一个新的对象、解构赋值某些字段、手动过滤敏感字段等,而且几乎成为了所有AI的<strong>默认做法</strong>,但这种做法真的就是万全之策吗?在项目初期看似简单、符合直觉,甚至AI的上下文也能很好的记忆其属性和字段,但随着业务增长,它带来的问题痛点也愈发明显:</p>
<ul>
<li>需要写大量重复代码;</li>
<li>每次实体属性变更都要手动同步响应处理逻辑;</li>
<li>容易忘记某些字段导致响应结构不一致;</li>
<li>无法利用 ORM 元数据、类型系统带来的优势。</li>
</ul>
<p><code>NestJS</code> 与 <code>class-transformer</code> 结合提供了一套更优雅的响应控制机制,使得序列化(serialization)成为可声明、可复用的过程,从而大幅提升代码质量及开发体验。</p>
<h3 id="序列化与反序列化的概念"><a href="#序列化与反序列化的概念" class="headerlink" title="序列化与反序列化的概念"></a>序列化与反序列化的概念</h3><p><strong>想要清晰的解决问题,就必须掌握好扎实的技术基础,否则我们甚至都无法清晰的把自己的想法表述给AI,甚至都不知道我们绞尽脑汁的方案,早有智者给我铺好了路。</strong></p>
<p>在服务端的内存中,数据通常以复杂的对象形式存在,例如包含各种方法和数据库元数据的ORM实体类实例。然而,HTTP协议本质上只能传输文本流。</p>
<p>将内存中的对象状态转换为可以存储或传输的格式(如字符串)的过程,称为<strong>序列化</strong>。</p>
<p>反之,将接收到的文本数据还原为内存中对象的实例的过程,称为<strong>反序列化</strong>。</p>
<p>在浏览器环境和HTTP协议的交互中,<code>JSON</code>(JavaScript Object Notation)承担了最核心的数据媒介作用。服务端将实体对象序列化为JSON字符串,通过HTTP响应发送给浏览器;浏览器接收后将其解析为JavaScript对象供前端逻辑使用。</p>
<h3 id="业务开发中的真实痛点"><a href="#业务开发中的真实痛点" class="headerlink" title="业务开发中的真实痛点"></a>业务开发中的真实痛点</h3><p>理解了上述基础后,我们来看看在实际NestJS业务开发中,处理API响应时经常面临的三个真实痛点:</p>
<p>第一,手动构建响应对象不够优雅。在控制器层返回数据时,开发者经常需要手动创建新对象并进行解构赋值,将ORM实例的属性逐一填入。这种做法需要编写大量样板代码,无法利用ORM工具和类本身的特性,且在属性众多时极易发生漏写或错写。</p>
<p>第二,ORM实体与响应结果存在割裂。随着业务迭代,数据库表结构会发生变化,ORM层中的实体类也会随之更新。如果我们在控制器中采用手动赋值的方式,即使实体类增加了新属性,只要控制器代码没有同步修改,新属性就无法自动体现在API响应中。这增加了维护成本,容易导致前后端数据不同步。</p>
<p>第三,DTO与实体的重复声明问题。为了规范接口,我们通常会编写DTO(数据传输对象)来约束请求和响应格式。在很多场景下,我们希望响应数据基于数据库实体,但需要遮蔽部分敏感字段。如果为了实现遮蔽而重新手写一个DTO,会导致DTO类和实体类中存在大量重复的字段声明。这不仅违背了DRY(Don’t Repeat Yourself)原则,手动解构赋值也无法从根源上保障数据结构的严谨性。</p>
<h3 id="解决方案:类型映射与序列化的结合"><a href="#解决方案:类型映射与序列化的结合" class="headerlink" title="解决方案:类型映射与序列化的结合"></a>解决方案:类型映射与序列化的结合</h3><p>为了解决上述问题,最佳实践是引入<code>class-transformer</code>库的装饰器,配合NestJS内置的类型映射(<code>Mapped Types</code>)功能以及<code>plainToInstance</code>序列化方法。</p>
<p>NestJS本身并不直接操作 <code>JSON.stringify</code>,而是通过 <code>class-transformer</code> 库,通过装饰器在类上声明序列化行为,并在响应时应用这些行为,利用<code>class-transformer</code>,我们可以实现对JavaScript类的更深入的操作。</p>
<p>基础概念来自官方文档:<br><a href="%22https://docs.nestjs.cn/techniques/serialization%22">序列化</a><br><a href="%22%22https://docs.nestjs.cn/graphql/mapped-types#%E6%98%A0%E5%B0%84%E7%B1%BB%E5%9E%8B">类型映射</a></p>
<ul>
<li><strong>@Exclude</strong>:排除字段,不出现在序列化输出;</li>
<li><strong>@Expose</strong>:显式声明字段可序列化;</li>
<li><strong>plainToInstance / instanceToPlain</strong>:在 plain object 与 Class 实例之间转换对象;</li>
<li><strong>ClassSerializerInterceptor</strong>:拦截器,在控制器返回对象时执行序列化转换。</li>
</ul>
<p>这种做法带来的好处在于,它把字段控制的逻辑放到了类定义层,而不是分散在每个 API 的手动处理逻辑里。</p>
<p>通过这套组合拳,我们可以将ORM实体作为唯一的真实数据源(Single Source of Truth),让DTO直接继承实体的定义,再通过序列化机制自动过滤敏感信息,从而实现高度复用和自动化的响应处理。</p>
<h4 id="解决方案:结合-class-transformer-的序列化策略"><a href="#解决方案:结合-class-transformer-的序列化策略" class="headerlink" title="解决方案:结合 class-transformer 的序列化策略"></a>解决方案:结合 class-transformer 的序列化策略</h4><p>为了解决这些问题,可以借助以下几种 NestJS 级工具:</p>
<h4 id="•-利用-Expose-与-Exclude-控制序列化字段"><a href="#•-利用-Expose-与-Exclude-控制序列化字段" class="headerlink" title="• 利用 @Expose 与 @Exclude 控制序列化字段"></a>• 利用 <code>@Expose</code> 与 <code>@Exclude</code> 控制序列化字段</h4><p>通过在实体类中使用 <code>class-transformer</code> 的装饰器,可以精确控制哪些字段最终出现在序列化输出。例如:</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-meta">@Exclude</span>()<br>password!: <span class="hljs-built_in">string</span>;<br></code></pre></td></tr></table></figure>
<p>使得 <code>password</code> 字段不会包含在 HTTP 相应响应里。</p>
<p>在你的实体定义中,采用了 <code>@Expose()</code> 和 <code>@Exclude()</code> 有选择地声明字段:</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-meta">@Expose</span>()<br><span class="hljs-meta">@ApiProperty</span>(...)<br>id!: <span class="hljs-built_in">number</span>;<br><br><span class="hljs-meta">@Exclude</span>()<br><span class="hljs-meta">@property</span>()<br>password!: <span class="hljs-built_in">string</span>;<br><br><span class="hljs-meta">@Exclude</span>()<br><span class="hljs-meta">@property</span>()<br><span class="hljs-attr">hashedRefreshToken</span>?: <span class="hljs-built_in">string</span>;<br></code></pre></td></tr></table></figure>
<p>这种方式避免了每次显式构造返回对象的必要,序列化过程可以自动过滤敏感字段。</p>
<h4 id="•-使用-mapped-types-简化-DTO-定义"><a href="#•-使用-mapped-types-简化-DTO-定义" class="headerlink" title="• 使用 mapped-types 简化 DTO 定义"></a>• 使用 mapped-types 简化 DTO 定义</h4><p>NestJS 官方提供的映射类型工具(如 <code>OmitType</code>、<code>PartialType</code> 等)可以在已有类上派生新的 DTO,而不需要重复声明字段。</p>
<p>在用户模块中,我们就定义了多个基于实体的 DTO:</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">UserResponseDto</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">OmitType</span>(<span class="hljs-title class_">User</span>, [<br> <span class="hljs-string">'password'</span>,<br> <span class="hljs-string">'hashedRefreshToken'</span>,<br>] <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>) {}<br></code></pre></td></tr></table></figure>
<p>这样定义后,响应 DTO 自动继承了实体的字段,排除了敏感项。</p>
<h4 id="•-使用-plainToInstance-进行纯净转换"><a href="#•-使用-plainToInstance-进行纯净转换" class="headerlink" title="• 使用 plainToInstance 进行纯净转换"></a>• 使用 <code>plainToInstance</code> 进行纯净转换</h4><p>在 controller 层,我们调用进来 ORM 查询的实体对象通常是一个模型实例。为了确保最终输出符合 DTO 声明的序列化规则,需要显式执行转换:</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">return</span> <span class="hljs-title function_">plainToInstance</span>(<span class="hljs-title class_">MeResponseDto</span>, user, {<br> <span class="hljs-attr">excludeExtraneousValues</span>: <span class="hljs-literal">true</span>,<br>});<br></code></pre></td></tr></table></figure>
<p>这里:</p>
<ul>
<li><code>MeResponseDto</code> 是基于实体定义但排除了敏感字段的响应模型;</li>
<li><code>excludeExtraneousValues: true</code> 确保只输出有 <code>@Expose()</code> 或在 DTO 中声明的字段。</li>
</ul>
<h3 id="业务场景实战解析"><a href="#业务场景实战解析" class="headerlink" title="业务场景实战解析"></a>业务场景实战解析</h3><p>接下来,我们将结合真实场景的代码,逐步解析这套方案的落地过程。在我遇到的真实开发场景中,项目要求我开发<code>GET user/me</code>的RESTful API,在这个API下,我们需要实现获取当前用户信息的API,并且要求在返回结果中严格剔除用户的密码和哈希处理后的刷新令牌。</p>
<h4 id="1-实体类的基础定义与装饰器配置"><a href="#1-实体类的基础定义与装饰器配置" class="headerlink" title="1. 实体类的基础定义与装饰器配置"></a>1. 实体类的基础定义与装饰器配置</h4><p>首先,我们查看数据库实体类的定义。在这个类中,我们不仅定义了数据库映射关系,同时引入了<code>class-transformer</code>的<code>Expose</code>和<code>Exclude</code>装饰器。</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// user.entity.ts</span><br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Entity</span>, <span class="hljs-title class_">PrimaryKey</span>, <span class="hljs-title class_">Property</span>, <span class="hljs-title class_">Unique</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@mikro-orm/core'</span>;<br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">ApiProperty</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/swagger'</span>;<br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Exclude</span>, <span class="hljs-title class_">Expose</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'class-transformer'</span>;<br><br><span class="hljs-meta">@Entity</span>({ <span class="hljs-attr">tableName</span>: <span class="hljs-string">'users'</span> })<br><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">User</span> {<br> <span class="hljs-meta">@Expose</span>()<br> <span class="hljs-meta">@ApiProperty</span>({ <span class="hljs-attr">description</span>: <span class="hljs-string">'用户唯一标识ID'</span> })<br> <span class="hljs-meta">@PrimaryKey</span>()<br> id!: <span class="hljs-built_in">number</span>;<br><br> <span class="hljs-meta">@Expose</span>()<br> <span class="hljs-meta">@ApiProperty</span>({ <span class="hljs-attr">description</span>: <span class="hljs-string">'学校学号'</span> })<br> <span class="hljs-meta">@Unique</span>()<br> <span class="hljs-meta">@Property</span>({ <span class="hljs-attr">nullable</span>: <span class="hljs-literal">false</span> })<br> schoolId!: <span class="hljs-built_in">string</span>;<br><br> <span class="hljs-comment">// 关键点:使用Exclude标记敏感字段</span><br> <span class="hljs-meta">@Exclude</span>()<br> <span class="hljs-meta">@Property</span>()<br> password!: <span class="hljs-built_in">string</span>;<br><br> <span class="hljs-comment">// 关键点:同样使用Exclude标记刷新令牌</span><br> <span class="hljs-meta">@Exclude</span>()<br> <span class="hljs-meta">@Property</span>({ <span class="hljs-attr">nullable</span>: <span class="hljs-literal">true</span> })<br> <span class="hljs-attr">hashedRefreshToken</span>?: <span class="hljs-built_in">string</span>;<br><br> <span class="hljs-comment">// ... 其他属性省略</span><br>}<br></code></pre></td></tr></table></figure>
<p>在这里,<code>@Expose</code>表示该字段在序列化时应该被暴露(保留),而<code>@Exclude</code>则明确指示该字段在序列化过程中必须被剔除。这就在数据源头确立了序列化规则。</p>
<h4 id="2-使用类型映射构建DTO"><a href="#2-使用类型映射构建DTO" class="headerlink" title="2. 使用类型映射构建DTO"></a>2. 使用类型映射构建DTO</h4><p>确立了实体类后,我们不需要重新编写一个包含几十个字段的响应DTO。借助NestJS提供的<code>OmitType</code>(也可以是<code>PickType</code>等),我们可以直接从User实体中派生出新的DTO类。</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// user-response.dto.ts</span><br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">OmitType</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/swagger'</span>;<br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">User</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'../entities/user.entity'</span>;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">MeResponseDto</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">OmitType</span>(<span class="hljs-title class_">User</span>, [<br> <span class="hljs-string">'password'</span>,<br> <span class="hljs-string">'hashedRefreshToken'</span>,<br> <span class="hljs-string">'avatarUrl'</span>,<br> <span class="hljs-string">'createdAt'</span>,<br> <span class="hljs-string">'updatedAt'</span>,<br>] <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>) {}<br></code></pre></td></tr></table></figure>
<p>OmitType在类型层面去除了User类中的指定字段,生成了MeResponseDto。这种做法完全遵循DRY原则,当User实体新增了普通业务字段(如age)并标记为Expose时,MeResponseDto会自动继承该字段,无需任何额外修改,直接解决了上文提到的维护割裂问题。</p>
<h4 id="3-控制器层的数据处理与转换"><a href="#3-控制器层的数据处理与转换" class="headerlink" title="3. 控制器层的数据处理与转换"></a>3. 控制器层的数据处理与转换</h4><p>最后一步是在控制器中应用这些规则。我们获取到ORM返回的用户实例后,不再手动拼接对象,而是使用plainToInstance进行转换。</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// user.controller.ts</span><br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Controller</span>, <span class="hljs-title class_">Get</span>, <span class="hljs-title class_">Req</span>, <span class="hljs-title class_">UnauthorizedException</span>, <span class="hljs-title class_">UseGuards</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;<br><span class="hljs-keyword">import</span> { plainToInstance } <span class="hljs-keyword">from</span> <span class="hljs-string">'class-transformer'</span>;<br><span class="hljs-keyword">import</span> { <span class="hljs-title class_">MeResponseDto</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/user-response.dto'</span>;<br><span class="hljs-comment">// ... 其他导入省略</span><br><br><span class="hljs-meta">@Controller</span>(<span class="hljs-string">'user'</span>)<br><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">UserController</span> {<br> <span class="hljs-title function_">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-attr">userService</span>: <span class="hljs-title class_">UserService</span></span>) {}<br><br> <span class="hljs-meta">@UseGuards</span>(<span class="hljs-title class_">JwtAuthGuard</span>)<br> <span class="hljs-meta">@Get</span>(<span class="hljs-string">'me'</span>)<br> <span class="hljs-keyword">async</span> <span class="hljs-title function_">getMe</span>(<span class="hljs-meta">@Req</span>() <span class="hljs-attr">req</span>: <span class="hljs-title class_">Request</span>): <span class="hljs-title class_">Promise</span><<span class="hljs-title class_">MeResponseDto</span>> {<br> <span class="hljs-keyword">const</span> userPayload = req[<span class="hljs-string">'user'</span>];<br><br> <span class="hljs-comment">// 从数据库获取ORM实体实例</span><br> <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">userService</span>.<span class="hljs-title function_">findOneBySchoolId</span>(userPayload.<span class="hljs-property">schoolId</span>);<br><br> <span class="hljs-keyword">if</span> (!user) {<br> <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">UnauthorizedException</span>(<span class="hljs-string">'用户不存在'</span>);<br> }<br><br> <span class="hljs-comment">// 关键点:使用plainToInstance进行序列化转换</span><br> <span class="hljs-keyword">return</span> <span class="hljs-title function_">plainToInstance</span>(<span class="hljs-title class_">MeResponseDto</span>, user, {<br> <span class="hljs-attr">excludeExtraneousValues</span>: <span class="hljs-literal">true</span>,<br> });<br> }<br>}<br></code></pre></td></tr></table></figure>
<p>在这里,<code>plainToInstance</code>接收目标类<code>MeResponseDto</code>和原始数据<code>user</code>。配置项<code>excludeExtraneousValues</code>设为<code>true</code>是核心所在,它告诉转换器:只保留目标类中带有<code>@Expose</code>装饰器的属性,其余属性一律丢弃。</p>
<p>由于我们在<code>User</code>实体中对<code>password</code>等字段使用了<code>@Exclude</code>(未带有<code>@Expose</code>),在转换过程中这些敏感信息会被彻底剥离。最终返回给前端的JSON数据完全符合我们期望的安全结构。</p>
<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>通过在实体类中使用<code>@Expose</code>和<code>@Exclude</code>,结合NestJS的<code>Mapped Types</code>复用类型定义,并在控制器端通过<code>plainToInstance</code>进行数据清洗,我们建立了一套清晰且健壮的响应处理机制。这种做法消除了冗余代码,降低了因遗忘导致的敏感数据泄露风险。</p>
<p>即便是在AI时代,提高项目的工程化质量不仅使NestJS后端的开发体验更加工程化和优雅,也可以变相促进AI更好的利用仓库代码已有的最佳实践,从而提升整体项目的健壮性和可维护性。</p>
</div>
<hr>
<div>
<div class="post-metas my-3">
<div class="post-meta mr-3 d-flex align-items-center">
<i class="iconfont icon-category"></i>
<span class="category-chains">
<span class="category-chain">
<a href="/categories/backend/" class="category-chain-item">backend</a>
</span>
</span>
</div>
<div class="post-meta">
<i class="iconfont icon-tags"></i>
<a href="/tags/nodejs/" class="print-no-link">#nodejs</a>
<a href="/tags/backend/" class="print-no-link">#backend</a>
<a href="/tags/nestjs/" class="print-no-link">#nestjs</a>
<a href="/tags/fullstack/" class="print-no-link">#fullstack</a>
</div>
</div>
<div class="license-box my-3">
<div class="license-title">
<div>NestJS全栈实战笔记:序列化与更优雅的响应结果处理</div>
<div>http://arkpln.github.io/4238683975.html</div>
</div>
<div class="license-meta">
<div class="license-meta-item">
<div>Author</div>
<div>FangZhou</div>
</div>
<div class="license-meta-item license-meta-date">
<div>Posted on</div>
<div>February 21, 2026</div>
</div>
<div class="license-meta-item">
<div>Licensed under</div>
<div>
<a class="print-no-link" target="_blank" href="https://creativecommons.org/licenses/by/4.0/" rel="external nofollow noopener noreferrer">
<span class="hint--top hint--rounded" aria-label="BY - Attribution">
<i class="iconfont icon-cc-by"></i>
</span>
</a>
</div>
</div>
</div>
<div class="license-icon iconfont"></div>
</div>
<div style="width:100%;display:flex;justify-content:center;margin-bottom:1.5rem"><ins class="adsbygoogle" style="display:flex;justify-content:center;max-width:845px;width:100%;height:90px" data-ad-client="ca-pub-3240358922862141" data-ad-slot="6522434837"></ins><script> (adsbygoogle = window.adsbygoogle || []).push({}); </script></div>
<div class="post-prevnext my-3">
<article class="post-prev col-6">
<a href="/482054044.html" title="Vite-plus下一代统一前端工具链初体验">
<i class="iconfont icon-arrowleft"></i>
<span class="hidden-mobile">Vite-plus下一代统一前端工具链初体验</span>
<span class="visible-mobile">Previous</span>
</a>
</article>
<article class="post-next col-6">
<a href="/2862114693.html" title="NestJS全栈实战笔记:优雅处理Entity与DTO的映射与字段过滤">
<span class="hidden-mobile">NestJS全栈实战笔记:优雅处理Entity与DTO的映射与字段过滤</span>
<span class="visible-mobile">Next</span>
<i class="iconfont icon-arrowright"></i>
</a>
</article>
</div>
</div>
<article id="comments" lazyload>
<div id="valine"></div>
<script type="text/javascript">
Fluid.utils.loadComments('#valine', function() {
Fluid.utils.createScript('https://lib.baomitu.com/valine/1.5.1/Valine.min.js', function() {
var options = Object.assign(
{"appId":"mEwkD7geCX7RWNO8srhDE2oP-gzGzoHsz","appKey":"4EF76hyiVA1QILHZThVbfoat","path":"window.location.pathname","placeholder":null,"avatar":"retro","meta":["nick","mail","link"],"requiredFields":[],"pageSize":10,"lang":"zh-CN","highlight":false,"recordIP":false,"serverURLs":"","emojiCDN":null,"emojiMaps":null,"enableQQ":false,"el":"#vcomments"},
{
el: "#valine",
path: window.location.pathname
}
)
new Valine(options);
Fluid.utils.waitElementVisible('#valine .vcontent', () => {
var imgSelector = '#valine .vcontent img:not(.vemoji)';
Fluid.plugins.imageCaption(imgSelector);
Fluid.plugins.fancyBox(imgSelector);
})
});
});
</script>
<noscript>Please enable JavaScript to view the comments</noscript>
</article>
</article>
</div>
</div>
</div>
<div class="side-col d-none d-lg-block col-lg-2">
<aside class="sidebar" style="margin-left: -1rem">
<div id="toc">
<p class="toc-header">
<i class="iconfont icon-list"></i>
<span>Table of Contents</span>
</p>
<div class="toc-body" id="toc-body"></div>
</div>
</aside>
</div>
</div>
</div>
<a id="scroll-top-button" aria-label="TOP" href="#" role="button">
<i class="iconfont icon-arrowup" aria-hidden="true"></i>
</a>
<div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<div class="modal-content">
<div class="modal-header text-center">
<h4 class="modal-title w-100 font-weight-bold">Search</h4>
<button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body mx-3">
<div class="md-form mb-5">
<input type="text" id="local-search-input" class="form-control validate">
<label data-error="x" data-success="v" for="local-search-input">Keyword</label>
</div>
<div class="list-group" id="local-search-result"></div>
</div>
</div>
</div>
</div>
</main>
<footer>
<div class="footer-inner">
<div class="footer-content">
<a href="https://hexo.io" target="_blank" rel="external nofollow noopener noreferrer"><span>Hexo</span></a> <i class="iconfont icon-love"></i> <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="external nofollow noopener noreferrer"><span>Fluid</span></a>
</div>
<div class="statistics">
<span id="leancloud-site-pv-container" style="display: none">
总访问量
<span id="leancloud-site-pv"></span>
次
</span>
<span id="leancloud-site-uv-container" style="display: none">
总访客数
<span id="leancloud-site-uv"></span>
人
</span>
</div>
</div>
</footer>
<!-- Scripts -->
<script src="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.js"></script>
<link rel="stylesheet" href="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.css">
<script>
NProgress.configure({"showSpinner":false,"trickleSpeed":100})
NProgress.start()
window.addEventListener('load', function() {
NProgress.done();
})
</script>
<script src="https://lib.baomitu.com/jquery/3.6.4/jquery.min.js"></script>
<script src="https://lib.baomitu.com/twitter-bootstrap/4.6.1/js/bootstrap.min.js"></script>
<script src="/js/events.js"></script>
<script src="/js/plugins.js"></script>
<script src="https://lib.baomitu.com/typed.js/2.0.12/typed.min.js"></script>
<script>
(function (window, document) {
var typing = Fluid.plugins.typing;
var subtitle = document.getElementById('subtitle');
if (!subtitle || !typing) {
return;
}
var text = subtitle.getAttribute('data-typed-text');
typing(text);
})(window, document);
</script>
<script src="/js/img-lazyload.js"></script>
<script>
Fluid.utils.createScript('https://lib.baomitu.com/tocbot/4.20.1/tocbot.min.js', function() {
var toc = jQuery('#toc');
if (toc.length === 0 || !window.tocbot) { return; }
var boardCtn = jQuery('#board-ctn');
var boardTop = boardCtn.offset().top;
window.tocbot.init(Object.assign({
tocSelector : '#toc-body',
contentSelector : '.markdown-body',
linkClass : 'tocbot-link',
activeLinkClass : 'tocbot-active-link',
listClass : 'tocbot-list',
isCollapsedClass: 'tocbot-is-collapsed',
collapsibleClass: 'tocbot-is-collapsible',
scrollSmooth : true,
includeTitleTags: true,
headingsOffset : -boardTop,
}, CONFIG.toc));
if (toc.find('.toc-list-item').length > 0) {
toc.css('visibility', 'visible');
}
Fluid.events.registerRefreshCallback(function() {
if ('tocbot' in window) {
tocbot.refresh();
var toc = jQuery('#toc');
if (toc.length === 0 || !tocbot) {
return;
}
if (toc.find('.toc-list-item').length > 0) {
toc.css('visibility', 'visible');
}
}
});
});
</script>
<script src="https://lib.baomitu.com/clipboard.js/2.0.11/clipboard.min.js"></script>
<script>Fluid.plugins.codeWidget();</script>
<script>
Fluid.utils.createScript('https://lib.baomitu.com/anchor-js/5.0.0/anchor.min.js', function() {
window.anchors.options = {
placement: CONFIG.anchorjs.placement,
visible : CONFIG.anchorjs.visible
};
if (CONFIG.anchorjs.icon) {
window.anchors.options.icon = CONFIG.anchorjs.icon;
}
var el = (CONFIG.anchorjs.element || 'h1,h2,h3,h4,h5,h6').split(',');
var res = [];
for (var item of el) {
res.push('.markdown-body > ' + item.trim());
}
if (CONFIG.anchorjs.placement === 'left') {
window.anchors.options.class = 'anchorjs-link-left';
}
window.anchors.add(res.join(', '));
Fluid.events.registerRefreshCallback(function() {
if ('anchors' in window) {
anchors.removeAll();
var el = (CONFIG.anchorjs.element || 'h1,h2,h3,h4,h5,h6').split(',');
var res = [];
for (var item of el) {
res.push('.markdown-body > ' + item.trim());
}
if (CONFIG.anchorjs.placement === 'left') {
anchors.options.class = 'anchorjs-link-left';
}
anchors.add(res.join(', '));
}
});
});
</script>
<script>
Fluid.utils.createScript('https://lib.baomitu.com/fancybox/3.5.7/jquery.fancybox.min.js', function() {
Fluid.plugins.fancyBox();
});
</script>
<script>Fluid.plugins.imageCaption();</script>
<script defer src="/js/leancloud.js"></script>
<script src="/js/local-search.js"></script>
<!-- 主题的启动项,将它保持在最底部 -->
<!-- the boot of the theme, keep it at the bottom -->
<script src="/js/boot.js"></script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3240358922862141" crossorigin="anonymous"></script>
<noscript>
<div class="noscript-warning">Blog works best with JavaScript enabled</div>
</noscript>
</body>
</html>