停止编写样板代码:我如何构建代码生成器来自动化 NestJS 开发

发布: (2026年2月8日 GMT+8 16:46)
14 分钟阅读
原文: Dev.to

Source: Dev.to

停止编写样板代码!我如何构建一个代码生成器来自动化 NestJS 开发

在日常的 NestJS 项目中,我们经常会重复创建相同的文件结构、装饰器以及基本的 CRUD 代码。虽然 NestJS CLI 已经帮我们生成了一些骨架,但仍有大量的手动工作需要完成。本文将分享我是如何使用 Plop自定义脚本 搭建一个代码生成器,从而彻底摆脱重复的样板代码。


目录

  1. 为什么需要代码生成器?
  2. 技术选型
  3. 项目结构
  4. 实现细节
    • 4.1 配置 Plop
    • 4.2 编写模板文件
    • 4.3 动态变量与自定义逻辑
  5. 使用示例
  6. 进阶功能
  7. 结语

为什么需要代码生成器?

  • 重复劳动:每新增一个模块,都要手动创建 controllerservicemoduledto 等文件。
  • 一致性:手写代码时容易出现命名不统一、装饰器遗漏等问题。
  • 效率:在快速迭代的项目里,生成代码的速度直接影响开发进度。

举例:在一个普通的 CRUD 模块中,至少需要 56 个文件,手动创建大约需要 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.hbsmodule.hbsdto/*.hbs)遵循相同的命名规则,只是内容略有差异。

4.3 动态变量与自定义逻辑

  • kebabCasecamelCaseupperFirst 等是 Plop 内置的 Handlebars 辅助函数,能够自动把用户输入的 name 转换为不同的命名风格。
  • 若需要更复杂的转换(如中文转拼音),可以在 utils/string-utils.ts 中实现并在 plopfile.jssetHelper 暴露。

使用示例

  1. 安装依赖
npm i -D plop prettier
  1. package.json 中添加脚本
{
  "scripts": {
    "gen": "plop"
  }
}
  1. 运行生成器
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 中插入 importmodule 声明,可通过 add 动作修改现有文件。
多语言模板使用不同的模板文件夹(如 templates/entemplates/zh),根据用户选择加载对应模板。
自定义脚本plopfile.js 中使用 exec 动作调用 nest g servicenest 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 数组。
  • statusCodemessage:必填字段。

请求基类(例如分页、管理员请求)

// 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- 只包含业务逻辑。
- 返回原始数据负载(不进行响应包装)。

架构概览

Architecture diagram

第2阶段:构建生成器

在模式就绪后,我实现了一个 CLI 生成器。核心逻辑如下:

  1. 分析 DTO – 读取 DTO 定义文件。
  2. 确定方法 – 根据 DTO 名称推断 HTTP 方法(例如 CreateUserDto → POST)。
  3. 生成代码
    • 创建 Controller API 端点。
    • 脚手架 Service 框架代码。
    • 生成 Module 文件并自动在主模块中注册。
  4. 类型安全 – 使用 TypeScript 泛型,使 Service 返回的类型与 BaseResponse 所期望的完全一致。

确定性生成逻辑

该生成器 100 % 确定:它依赖严格的模式匹配和预定义模板,区别于可能不可预测的 AI 生成代码。每个字符都会恰到好处地出现在正确位置。

生成工作流

Generation workflow diagram

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 ControllerService 层。

Generated NestJS files

结论

构建代码生成器乍看可能像是“过度工程”,或是增加维护负担。然而,投资回报率非常可观。

  • 维护? 是的,工具需要维护。但有了 AI 的帮助,更新生成器比手动更新数百个文件要快得多。
  • 价值? 它把我从“仅仅是编码者”变成了“设计系统的工程师”。

如果你的团队正受到重复性任务的困扰,停止敲键盘。开始生成吧。

0 浏览
Back to Blog

相关文章

阅读更多 »

Calendar Feeds:一切的起点

当我住在贝尔法斯特时,我有一个问题:我想知道 Strand 电影院正在放映什么,而不必记得去查看他们的网站。我想 t...