cd ../log

Reducing backend boilerplate with generic services

Notes from experimenting with reusable CRUD behavior in NestJS and Fastify.

  • nestjs
  • fastify
  • backend

Backend projects often start with healthy repetition. A few controllers, a few services, a few DTOs, and everything is explicit. Then the project grows, and the same CRUD shape appears again and again.

Diagram showing generic CRUD behavior separated from explicit product logic
The useful abstraction is the one that removes repeated structure without hiding product decisions.

The pattern that repeats

Many resources need the same basic behavior:

  • create a record
  • read one or many records
  • update a record
  • delete or archive a record
  • validate input
  • filter and paginate results
  • return a predictable response

At first, repeating this code is fine. It keeps the system obvious. But after enough modules, the repetition stops adding clarity. It becomes maintenance weight.

That is the problem I wanted to explore with a generic web service approach.

The experiment

The idea was to use NestJS, Fastify, TypeScript, reflection, and metadata to generate reusable CRUD behavior while still leaving space for business rules.

The goal was not to make a magic backend. Magic is usually expensive later. The goal was to remove the boring parts that are easy to define and hard to justify writing again.

A generic layer can handle the shared shape:

  • route structure
  • basic service methods
  • common validation flow
  • response formatting
  • simple filters

But it should not swallow the product logic.

Belongs in the generic layerShould remain explicit
PaginationPricing rules
Basic filtersPayment workflow
Response formattingPermission decisions
Common CRUD methodsDomain-specific validation
type GenericResourceConfig = {
  model: string;
  searchableFields: string[];
  allowedFilters: string[];
  beforeCreate?: "domain hook";
};

Where abstraction should stop

The most important design question is not “can this be generic?” It is “should this remain explicit?”

CRUD can often be shared. A pricing rule, payment workflow, onboarding step, or permission decision should usually stay visible in application code. Those are not boilerplate. They are the product.

That boundary matters because a bad abstraction hides decisions. A good abstraction removes noise around decisions.

What I would keep

If I continue this experiment, I would keep the generic layer small and boring. It should be easy to inspect, easy to override, and easy to delete from a module that needs custom behavior.

The useful version of this idea is not a framework inside the framework. It is a tool that helps the backend stay explicit where it matters and faster where it does not.