停止编写样板代码:我如何构建代码生成器来自动化 NestJS 开发
Source: Dev.to
停止编写样板代码!我如何构建一个代码生成器来自动化 NestJS 开发
在日常的 NestJS 项目中,我们经常会重复创建相同的文件结构、装饰器以及基本的 CRUD 代码。虽然 NestJS CLI 已经帮我们生成了一些骨架,但仍有大量的手动工作需要完成。本文将分享我是如何使用 Plop 与 自定义脚本 搭建一个代码生成器,从而彻底摆脱重复的样板代码。
目录
为什么需要代码生成器?
- 重复劳动:每新增一个模块,都要手动创建
controller、service、module、dto等文件。 - 一致性:手写代码时容易出现命名不统一、装饰器遗漏等问题。
- 效率:在快速迭代的项目里,生成代码的速度直接影响开发进度。
举例:在一个普通的 CRUD 模块中,至少需要 5
6 个文件,手动创建大约需要 1015 分钟。使用生成器后,这个过程可以在 5 秒 内完成。
技术选型
| 技术 | 作用 | 备注 |
|---|---|---|
| Node.js | 运行时环境 | 与 NestJS 项目保持同一语言(TypeScript) |
| TypeScript | 类型安全 | 直接使用项目的 tsconfig |
| Plop | 微型代码生成框架 | 简单易上手,支持自定义 Handlebars 模板 |
| Handlebars | 模板引擎 | 支持条件、循环等高级特性 |
| Prettier | 代码格式化 | 生成后自动格式化,保持代码风格统一 |
项目结构
my-nest-app/
├─ src/
│ ├─ app.module.ts
│ └─ ...(业务代码)
├─ generators/
│ ├─ plopfile.js # Plop 主入口
│ ├─ templates/
│ │ ├─ controller.hbs
│ │ ├─ service.hbs
│ │ ├─ module.hbs
│ │ └─ dto.hbs
│ └─ utils/
│ └─ string-utils.ts # 辅助函数(如驼峰转下划线)
├─ package.json
└─ tsconfig.json
实现细节
4.1 配置 Plop
generators/plopfile.js
module.exports = function (plop) {
// 注册自定义帮助函数
plop.setHelper('upperFirst', (text) => text.charAt(0).toUpperCase() + text.slice(1));
// 注册生成器
plop.setGenerator('module', {
description: '为 NestJS 项目生成完整的模块结构',
prompts: [
{
type: 'input',
name: 'name',
message: '模块名称(例如:user):',
},
{
type: 'confirm',
name: 'withCrud',
message: '是否生成 CRUD 相关文件(controller、service、dto)?',
default: true,
},
],
actions: (data) => {
const actions = [];
// 生成 module 文件
actions.push({
type: 'add',
path: 'src/{{kebabCase name}}/{{kebabCase name}}.module.ts',
templateFile: 'generators/templates/module.hbs',
});
if (data.withCrud) {
// controller
actions.push({
type: 'add',
path: 'src/{{kebabCase name}}/{{kebabCase name}}.controller.ts',
templateFile: 'generators/templates/controller.hbs',
});
// service
actions.push({
type: 'add',
path: 'src/{{kebabCase name}}/{{kebabCase name}}.service.ts',
templateFile: 'generators/templates/service.hbs',
});
// dto
actions.push({
type: 'addMany',
destination: 'src/{{kebabCase name}}/dto',
base: 'generators/templates/dto',
templateFiles: 'generators/templates/dto/*.hbs',
});
}
// 自动格式化
actions.push({ type: 'prettify' });
return actions;
},
});
};
4.2 编写模板文件
下面以 controller.hbs 为例,展示如何使用 Handlebars 语法:
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { {{upperFirst name}}Service } from './{{kebabCase name}}.service';
import { Create{{upperFirst name}}Dto } from './dto/create-{{kebabCase name}}.dto';
import { Update{{upperFirst name}}Dto } from './dto/update-{{kebabCase name}}.dto';
@Controller('{{kebabCase name}}')
export class {{upperFirst name}}Controller {
constructor(private readonly {{camelCase name}}Service: {{upperFirst name}}Service) {}
@Post()
create(@Body() createDto: Create{{upperFirst name}}Dto) {
return this.{{camelCase name}}Service.create(createDto);
}
@Get()
findAll() {
return this.{{camelCase name}}Service.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.{{camelCase name}}Service.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateDto: Update{{upperFirst name}}Dto) {
return this.{{camelCase name}}Service.update(+id, updateDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.{{camelCase name}}Service.remove(+id);
}
}
其他模板(service.hbs、module.hbs、dto/*.hbs)遵循相同的命名规则,只是内容略有差异。
4.3 动态变量与自定义逻辑
- kebabCase、camelCase、upperFirst 等是 Plop 内置的 Handlebars 辅助函数,能够自动把用户输入的
name转换为不同的命名风格。 - 若需要更复杂的转换(如中文转拼音),可以在
utils/string-utils.ts中实现并在plopfile.js中setHelper暴露。
使用示例
- 安装依赖
npm i -D plop prettier
- 在
package.json中添加脚本
{
"scripts": {
"gen": "plop"
}
}
- 运行生成器
npm run gen
随后会出现交互式提示:
? 模块名称(例如:user): product
? 是否生成 CRUD 相关文件(controller、service、dto)? Yes
执行完毕后,你会在 src/product/ 目录下看到如下结构:
src/product/
├─ product.controller.ts
├─ product.service.ts
├─ product.module.ts
└─ dto/
├─ create-product.dto.ts
└─ update-product.dto.ts
所有文件已经通过 Prettier 自动格式化,直接可以在 NestJS 项目中使用。
进阶功能
| 功能 | 实现思路 |
|---|---|
| 自动注册模块 | 在 app.module.ts 中插入 import 与 module 声明,可通过 add 动作修改现有文件。 |
| 多语言模板 | 使用不同的模板文件夹(如 templates/en、templates/zh),根据用户选择加载对应模板。 |
| 自定义脚本 | 在 plopfile.js 中使用 exec 动作调用 nest g service、nest g controller 等 Nest CLI 命令,实现混合生成。 |
| 测试文件生成 | 为每个生成的业务文件同步创建对应的 .spec.ts,并预置基本的 Jest 测试框架。 |
结语
通过 Plop + Handlebars 的组合,我们可以把繁琐的 NestJS 模块创建过程压缩到几秒钟内完成。这样不仅提升了团队的开发效率,也保证了代码风格的一致性。最重要的是,生成器本身是 可维护、可扩展 的——当业务需求变化时,只需要修改模板或添加新的动作即可。
如果你也在为 NestJS 项目中的重复劳动而头疼,强烈建议尝试本文的方案,甚至可以把它作为公司内部的 脚手架,让每位开发者都受益。
祝编码愉快 🚀
摘要
- 识别出重复的编码模式,并构建了一个 代码生成器 来实现自动化。
- 实现了代码的一致性、更易维护,并显著加快了开发周期。
- 利用 AI 协助维护生成器本身,降低了工具维护的开销。
介绍:为什么我决定停止手动编码
作为一名后端工程师,我一直在与重复的样板代码作斗争。每次创建新功能时,我都在一遍又一遍地输入相同的结构。我问自己:
“如果我只专注于业务逻辑和实现,开发速度不是会快很多吗?”
在使用 gRPC 时,我迎来了 “啊哈!” 的时刻。我看到基本代码可以从 .proto 文件自动生成。由于 Node.js 生态系统(NestJS、React、Next.js)已经采用了代码生成工具,我决定构建自己的 Custom Code Generator,以适配我们团队的特定架构。
第 1 阶段:标准化再自动化
在构建生成器之前,我需要先定义严格的代码模式——你无法自动化混乱。我采用了标准的 Controller → Service → Repository 模式,并对数据传输对象(DTO)实施了严格的规则。
1. 标准化响应与请求 DTO
响应示例
{
"statusCode": 200,
"message": "Success",
"data": {
"id": 1,
"name": "John Doe"
}
}
data:始终为对象。列表时包含items数组。statusCode与message:必填字段。
请求基类(例如分页、管理员请求)
// Example: Composing DTOs using IntersectionType
export class DomainGetDto extends IntersectionType(
BasePaginationRequestDto,
AdminRequestDto,
) {}
class DomainResponseData {
@ApiProperty({ item: DomainResponseDataItem, isArray: true })
items: DomainResponseDataItem[];
}
export class DomainResponse extends BaseResponse {
@ApiProperty({ type: DomainResponseData })
data: DomainResponseData;
}
2. 为 Controller 与 Service 定义职责
| 层级 | 职责 |
|---|---|
| Controller | - 接收 DTO(查询/请求体)并转发给 Service。 - 将 Service 返回的 data 字段包装在 BaseResponse 中返回。- 处理认证守卫和日志记录。 |
| Service | - 只包含业务逻辑。 - 返回原始数据负载(不进行响应包装)。 |
架构概览

第2阶段:构建生成器
在模式就绪后,我实现了一个 CLI 生成器。核心逻辑如下:
- 分析 DTO – 读取 DTO 定义文件。
- 确定方法 – 根据 DTO 名称推断 HTTP 方法(例如
CreateUserDto → POST)。 - 生成代码
- 创建 Controller API 端点。
- 脚手架 Service 框架代码。
- 生成 Module 文件并自动在主模块中注册。
- 类型安全 – 使用 TypeScript 泛型,使 Service 返回的类型与
BaseResponse所期望的完全一致。
确定性生成逻辑
该生成器 100 % 确定:它依赖严格的模式匹配和预定义模板,区别于可能不可预测的 AI 生成代码。每个字符都会恰到好处地出现在正确位置。
生成工作流

AI 在开发中的角色
虽然生成器本身遵循严格的规则,但我使用 AI 来构建和维护生成器。编写 AST 解析器或复杂的正则表达式模式非常繁琐;AI 帮助根据我定义的模式生成这些脚本。工作流程变为:
人类定义模式 → AI 编写生成器代码 → 生成器编写产品代码
第三阶段:结果
实施代码生成器带来了立竿见影的好处。
1. 专注业务逻辑
开发者不再需要担心样板代码。除非出现特殊的边缘情况,他们只需关注 Service 层。生成器还会搭建 单元测试,因此开发者可以直接进入实现和测试阶段。
2. 一致性与更快的代码审查
常见的审查问题消失了:
- 他们是否导入了正确的模块?
- 命名规范是否正确?
- 他们是否继承了
BaseResponse?
由于代码保证了一致性,审查者可以仅专注于逻辑,从而大幅缩短审查时间。
3. 加速开发速度
以前,样板代码和测试文件的设置会消耗大量时间。现在则是瞬间完成。
Bonus: 使用 Protobuf 自动化 gRPC
对于 gRPC,流程更加顺畅。不同于 REST——必须从 DTO 名称推断意图——gRPC 提供 .proto 文件,严格定义服务和消息,使生成器能够在无需猜测的情况下生成 client‑ 和 server‑side 代码。
定义服务和消息
我创建了一个 build‑proto 命令,它:
- 运行
protoc来构建基础类型。 - 读取
.proto定义。 - 自动生成对应于 Proto 定义的 NestJS Controller 和 Service 层。

结论
构建代码生成器乍看可能像是“过度工程”,或是增加维护负担。然而,投资回报率非常可观。
- 维护? 是的,工具需要维护。但有了 AI 的帮助,更新生成器比手动更新数百个文件要快得多。
- 价值? 它把我从“仅仅是编码者”变成了“设计系统的工程师”。
如果你的团队正受到重复性任务的困扰,停止敲键盘。开始生成吧。