DEV Community

Cover image for Stop Writing Boilerplate: How I Built a Code Generator to Automate NestJS Development
Jaesang Lee
Jaesang Lee

Posted on

Stop Writing Boilerplate: How I Built a Code Generator to Automate NestJS Development

Summary

  1. Identified repetitive coding patterns and built a Code Generator to automate them.
  2. Achieved code consistency, easier maintenance, and significantly faster development cycles.
  3. Leveraged AI to assist in maintaining the generator itself, reducing the overhead of tool maintenance.

Introduction: Why I Decided to Stop Coding (Manually)

As a backend engineer, I found myself constantly battling with repetitive boilerplate code. Every time I created a new feature, I was typing the same structures over and over again. I asked myself: "Wouldn't development be much faster if I could just focus on the business logic and implementation?"

The "Aha!" moment came while working with gRPC. I saw how basic code was automatically generated based on .proto files. Since the Node.js ecosystem (NestJS, React, Next.js) already embraces code generation tools, I decided to build my own Custom Code Generator tailored to our team's specific architecture.


Phase 1: Standardization Before Automation

Before building the generator, I needed to define a strict code pattern. You cannot automate chaos. I adopted the standard Controller-Service-Repository pattern and enforced strict rules for data transfer objects (DTOs).

1. Standardizing Response & Request DTOs

I created a consistent structure for all API responses.

{
  "statusCode": 200,
  "message": "Success",
  "data": {
    "id": 1,
    "name": "John Doe"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • data: Always an Object. For lists, it contains an items array.
  • statusCode & message: Mandatory fields.

For Requests, I created base classes for common patterns like Pagination and Admin requests.

// 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;
}
Enter fullscreen mode Exit fullscreen mode

2. Defining Roles for Controller & Service

I clearly separated the responsibilities to make the pattern predictable for the generator.

  • Controller:
    • Receives DTOs (Query/Body) and passes them directly to the Service.
    • Returns the data field from the Service wrapped in the BaseResponse.
    • Handles Auth guards and Logging.
  • Service:
    • Focuses purely on Business Logic.
    • Returns only the data payload.

Architecture Overview


Phase 2: Building the Generator

With the patterns in place, I built the generator CLI. The core logic is as follows:

  1. Analyze DTOs: It reads the DTO definition files.
  2. Determine Method: Based on the DTO name (e.g., CreateUserDto -> POST), it decides the HTTP method.
  3. Generate Code:
    • Creates the Controller API endpoints.
    • Creates the Service boilerplate.
    • Generates the Module file and automatically registers it to the Main Module.
  4. Type Safety: It uses TypeScript Generics to ensure the Service returns the correct data type expected by the BaseResponse.

Deterministic Generation Logic

I built the generator to be 100% deterministic using strict pattern matching. Unlike AI-generated code which can be unpredictable, this tool uses predefined templates to ensure every character is exactly where it should be.

Generation Workflow

Role of AI in Development

While the generator itself runs on strict rules, I leveraged AI to build and maintain the generator. Writing AST parsers or complex regex patterns for the generator can be tedious. I used AI to generate these "generator scripts" based on the patterns I defined. This hybrid approach—Human defines the pattern, AI writes the generator code, Generator writes the product code—proved to be highly efficient.


Phase 3: The Results

Implementing this Code Generator brought immediate benefits to the team.

1. Focus on Business Logic

Developers no longer worry about boilerplate. Unless there's a special edge case, they only need to touch the Service layer. Since the generator also scaffolds Unit Tests, developers can jump straight into implementing logic and testing it.

2. Consistency & Faster Code Reviews

The biggest hidden cost in development is often Code Review.

  • Did they import the right module?
  • Is the naming convention correct?
  • Did they extend the BaseResponse?

With the generator, these questions vanished. The code is guaranteed to be consistent. Reviewers can now focus entirely on the logic, drastically reducing review time.

3. Accelerated Development Speed

Writing boilerplate and setting up test files used to take up significant time. Now, it's instant.


Bonus: Automating gRPC with Protobuf

For gRPC, the process was even smoother. Unlike REST, where I had to infer intent from DTO names, gRPC has .proto files that strictly define the Service and Messages.

I created a build-proto command that:

  1. Runs protoc to build the base types.
  2. Reads the .proto definitions.
  3. Automatically generates the NestJS Controller and Service layers mapping to the Proto definitions.


Conclusion

Building a Code Generator might seem like "over-engineering" at first, or an added maintenance burden. However, the ROI has been massive.

  • Maintenance? Yes, the tool needs maintenance. But with AI assistance, updating the generator is faster than manually updating hundreds of files.
  • Value? It transformed me from "just a coder" to an "engineer who designs systems."

If your team is suffering from repetitive tasks, stop typing. Start generating.

Top comments (0)