DEV Community

Cover image for Prisma Generator NestJS DTO — pluggable DTOs with annotations and custom generators
Tommaso Meli
Tommaso Meli

Posted on

Prisma Generator NestJS DTO — pluggable DTOs with annotations and custom generators

If you build NestJS APIs on top of Prisma, you've probably felt the friction: your schema is the source of truth, but your DTOs, validation rules, and Swagger metadata live somewhere else — and they drift.

Most Prisma DTO generators solve the basics (Create / Update / Entity classes with decorators). That's useful, but the moment you need custom output — audit metadata, GraphQL types, RBAC manifests, your own validators — you're back to hand-written glue code.

@tommasomeli/prisma-generator-nestjs-dto is a Prisma generator built for that second phase. It emits the usual NestJS DTOs, then gets out of your way — with a plugin API, annotation-driven decorators, and a type-safe config file when the Prisma schema block isn't enough.

What you get out of the box

For every Prisma model User, run npx prisma generate and you get:

src/generated/nestjs-dto/
  user/
    user.entity.ts
    create-user.dto.ts
    update-user.dto.ts
    index.ts
  index.ts
Enter fullscreen mode Exit fullscreen mode

Each file ships with:

  • class-validator decorators (@IsString, @IsOptional, …)
  • @nestjs/swagger metadata (@ApiProperty, @ApiHideProperty, …)
  • Self-contained imports — relation DTOs, @DtoOverrideType targets, and annotation arguments are resolved automatically

Configure it directly in schema.prisma:

generator nestjsDto {
  provider           = "prisma-generator-nestjs-dto"
  output             = "../src/generated/nestjs-dto"
  outputType         = "class"
  outputStructure    = "nestjs"
  fileNamingStrategy = "kebab"
  reExport           = "true"
  classValidator     = "true"
  swaggerDocs        = "true"
  prettier           = "true"
}
Enter fullscreen mode Exit fullscreen mode

Install:

npm i -D @tommasomeli/prisma-generator-nestjs-dto
Enter fullscreen mode Exit fullscreen mode

Annotations — control visibility without touching generated code

Triple-slash comments (///) above models and fields drive the built-in generators. No post-processing, no manual edits.

model User {
  id           Int    @id @default(autoincrement())
  email        String @unique
  name         String
  /// @DtoHidden
  passwordHash String
  /// @DtoReadOnly
  createdAt    DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode
Annotation Effect
@DtoHidden Hide everywhere
@DtoReadOnly Exclude from Create and Update DTOs
@DtoEntityHidden Hide in the Entity (API response)
@DtoCreateHidden / @DtoUpdateHidden Hide in one DTO only
@DtoOverrideType(MyType) Override the TypeScript type (auto-imported)
@DtoIgnoreModel Skip the model entirely

You can also bind your own validators and decorators via config — more on that below.

The plugin system — when built-in DTOs aren't enough

The differentiator is extraGenerators: drop in any class extending BaseGenerator and it runs alongside the built-ins in the same pipeline.

A plugin receives:

  • The parsed model graph (with annotations on every field)
  • Resolved config (extraDecorators, extraValidators, extraImports, …)
  • Import-merging helpers (addImport, formatImports, getTemplate)

Here's a minimal plugin that reuses the built-in renderer to emit an Entity without class-validator:

import { isEntityHidden } from '@tommasomeli/prisma-generator-nestjs-dto';
import { BaseGenerator } from '@tommasomeli/prisma-generator-nestjs-dto';
import type { Field, File, Model } from '@tommasomeli/prisma-generator-nestjs-dto';

export default class EntityDtoGenerator extends BaseGenerator {
  filePrefix = '';
  fileSuffix = '.entity';
  classPrefix = '';
  classSuffix = '';

  async generate(): Promise<File[]> {
    return this.models.map((model) => {
      const filteredFields = model.fields.filter((f: Field) => !isEntityHidden(f));
      const processedModel: Model = { ...model, fields: filteredFields as Field[] };
      const outputPath = this.getPath(model);
      return {
        path: outputPath,
        content: this.getTemplate({ model: processedModel, classValidator: false, outputPath }),
      };
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Lifecycle hooks

Plugins can hook the full run:

  • beforeAll(models) — mutate the shared model list before any generator runs (shared pre-pass)
  • afterAll(files) — append barrels, audit reports, or aggregated indexes after all generators finish

TypeScript plugins, no precompilation

Point extraGenerators at a .ts file and the generator loads it via jiti. No build step required for your plugin.

Real example: custom @Auditable annotation

The repo includes a runnable example under examples/blog/.

Schema — annotate models with a custom @Auditable name:

/// @Auditable("user_audit")
model User {
  id           Int      @id @default(autoincrement())
  email        String   @unique
  /// @DtoHidden
  passwordHash String
  posts        Post[]
}

/// @Auditable("post_audit")
model Post {
  id       Int    @id @default(autoincrement())
  title    String
  author   User   @relation(fields: [authorId], references: [id])
  authorId Int
}
Enter fullscreen mode Exit fullscreen mode

Config — register the annotation and the plugin:

import { from, type GeneratorConfigFile } from '@tommasomeli/prisma-generator-nestjs-dto';

export default {
  extraAnnotations: ['Auditable'],
  extraGenerators: from('./generators/audit-generator.ts', ['AuditGenerator']),
} satisfies GeneratorConfigFile;
Enter fullscreen mode Exit fullscreen mode

Output — alongside the usual DTOs, you get per-model audit metadata and an aggregated index:

generated/
  user/user.audit.ts
  post/post.audit.ts
  audit-index.ts    ← emitted by AuditGenerator#afterAll
Enter fullscreen mode Exit fullscreen mode

Type-safe external config

Prisma's generator block is great for simple flags, but it can't express nested objects or multi-line arrays. For anything richer, use a configFile:

generator nestjsDto {
  provider   = "prisma-generator-nestjs-dto"
  output     = "../generated"
  configFile = "../nestjs-dto.config.ts"
}
Enter fullscreen mode Exit fullscreen mode

The from() helper validates paths and named exports at compile time (the import closure is never invoked at runtime):

import { from, fromNamespace, type GeneratorConfigFile } from '@tommasomeli/prisma-generator-nestjs-dto';

export default {
  extraValidators: from(() => import('src/common/validators'), ['IsUnique', 'IsStrongPassword']),
  extraDecorators: from(() => import('src/common/decorators'), ['Trim', 'Sanitize']),
  extraScalars: {
    Decimal: { ts: 'Decimal', from: 'decimal.js' },
    Json:    { ts: 'MyJson', from: 'src/json', apiType: 'Object' },
  },
} satisfies GeneratorConfigFile;
Enter fullscreen mode Exit fullscreen mode

Then wire annotations in the schema:

model User {
  /// @IsUnique()
  email    String @unique
  /// @IsStrongPassword({ minLength: 10 })
  password String
  /// @Trim()
  name     String
}
Enter fullscreen mode Exit fullscreen mode

If a custom validator collides by name with a built-in (IsBoolean, ApiProperty, …), your module wins for that symbol only.

Optional runtime manifest

Enable emitManifest = "true" to get:

  • manifest.tsRecord<Prisma.ModelName, { primaryKey, entityFields, relations }> for select builders, audit middleware, RBAC field lists
  • model-entity-map.ts — type-only map from model names to Entity classes

Useful when you need schema-aware runtime logic without parsing Prisma DMMF yourself.

How it compares

This generator Typical Prisma NestJS DTO generators
Create / Update / Entity DTOs
Swagger + class-validator
Annotation-driven hide / readonly / type override partial
Pluggable sub-generators
Custom annotations for plugins
Override built-in imports by name
Type-safe external configFile
Optional runtime manifest

Try it

npm i -D @tommasomeli/prisma-generator-nestjs-dto
npx prisma generate
Enter fullscreen mode Exit fullscreen mode

Issues and PRs are welcome — MIT licensed.

If this saves you time on your NestJS + Prisma stack, a ⭐ on the repo (or a coffee ☕) goes a long way.

Top comments (0)