DTO validation profiling
Inspect successful and failed DTO validations - class-validator or zod - in the profiler's Validator panel.
This tutorial shows how to add the validator collector to a NestJS application. The collector is validator-agnostic; this walkthrough uses class-validator (the default), and a nestjs-zod variant is shown below.
Prerequisites
@eleven-labs/nest-profilerinstalled and configured- A validation library. This tutorial uses
class-validator+class-transformer, which are required only for the default class-validator pipe - they are not dependencies of@eleven-labs/nest-profiler-validatoritself. For the nestjs-zod variant, installnestjs-zod+zodinstead.
Validation stays app-owned
You install the validation pipe in main.ts with createProfilerValidationPipe(...), and
ValidatorCollectorModule.forRoot() contributes only the panel. Validation runs independently of
the profiler, so the panel can be gated on/off per environment while validation always runs.
Step 1 - Install the package
pnpm add @eleven-labs/nest-profiler-validator@alpha
# this tutorial uses the default class-validator pipe:
pnpm add class-validator class-transformerStep 2 - Install the pipe and register the panel
Install the validation pipe in your bootstrap. createClassValidatorPipe (rather than a bare new ValidationPipe()) attaches the raw ValidationError[] the panel reads, so violations show per property:
import {
createProfilerValidationPipe,
createClassValidatorPipe,
} from '@eleven-labs/nest-profiler-validator';
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
createProfilerValidationPipe(createClassValidatorPipe({ whitelist: true, transform: true })),
);Then register the Validator panel — forRoot() adds only the collector (no APP_PIPE), so it gates like every other collector:
import { Module } from '@nestjs/common';
import { ConditionalModule } from '@nestjs/config';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { ValidatorCollectorModule } from '@eleven-labs/nest-profiler-validator';
const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true';
@Module({
imports: [
ConditionalModule.registerWhen(ProfilerModule.forRoot({ isGlobal: true }), isProfilerEnabled),
ConditionalModule.registerWhen(ValidatorCollectorModule.forRoot(), isProfilerEnabled),
],
})
export class AppModule {}Step 3 - Create a DTO with validation constraints
Use value imports, not type imports
Always import decorator functions from class-validator as value imports (not import type). The
decorators must be executed at runtime to register metadata. Using import type strips them at
compile time.
import { IsString, IsNotEmpty, MinLength, MaxLength, IsOptional, IsUrl } from 'class-validator';
export class CreateArticleDto {
@IsString()
@IsNotEmpty()
@MinLength(5)
@MaxLength(120)
title: string;
@IsString()
@IsNotEmpty()
@MinLength(20)
body: string;
@IsOptional()
@IsUrl()
coverImageUrl?: string;
}Step 4 - Use the DTO in a controller
import { Body, Controller, Post } from '@nestjs/common';
import { CreateArticleDto } from './dto/create-article.dto';
@Controller('articles')
export class ArticlesController {
@Post()
create(@Body() dto: CreateArticleDto) {
return { message: 'Article created', title: dto.title };
}
}Step 5 - Test it
Valid request:
curl -i -X POST http://localhost:3000/api/v1/articles \
-H "Content-Type: application/json" \
-d '{"title": "My first article", "body": "This is the body of the article with enough content."}'Invalid request:
curl -i -X POST http://localhost:3000/api/v1/articles \
-H "Content-Type: application/json" \
-d '{"title": "Hi", "body": "Too short"}'Open /_profiler/{token} and click the Validator tab.
For the valid request, you will see:
- Source -
ArticlesController.create - DTO class -
CreateArticleDto - Status -
valid - Violations - none
For the invalid request, you will see:
- Source -
ArticlesController.create - DTO class -
CreateArticleDto - Status -
invalid - Violations - one entry per failing property, listing the constraint names and messages
The toolbar badge shows the total count of validated DTOs, or N violations when at least one validation failed (e.g., 2 violations).
Capturing both valid and invalid validations
The collector records every validation that passes through the pipe - both successful and failed. This lets you:
- Confirm that valid payloads pass all constraints
- See exactly which constraints failed and on which properties
- Debug validation logic without adding temporary logging
Even when validation fails and NestJS returns a 400 Bad Request, the profile is still created
and the failed validation is recorded. Navigate to /_profiler to find the profile by timestamp.
Using a different validator (nestjs-zod)
The collector is validator-agnostic. To profile a nestjs-zod app instead, wrap its pipe — class-validator is never loaded:
import { ZodValidationPipe } from 'nestjs-zod';
import { createProfilerValidationPipe } from '@eleven-labs/nest-profiler-validator';
app.useGlobalPipes(createProfilerValidationPipe(new ZodValidationPipe()));import { createZodDto } from 'nestjs-zod';
import { z } from 'zod';
const ArticleSchema = z.object({
title: z.string().min(5).max(120),
body: z.string().min(20),
coverImageUrl: z.string().url().optional(),
});
export class CreateArticleDto extends createZodDto(ArticleSchema) {}The Validator panel renders zod violations exactly the same way - one entry per failing property with its messages. A NestJS app uses a single global validation strategy, so use one validator at a time. createProfilerValidationPipe(inner, extractors?) also takes a custom extractor chain as its second argument.
Testing the app manually
Since the pipe lives in main.ts, mirror the same useGlobalPipes(...) call when you boot the
app yourself in e2e tests (Test.createTestingModule(...).createNestApplication()).
How it works
ProfilerValidationPipe wraps the validation pipe you configure: it records a valid/invalid entry into the active request profile and normalizes violations through duck-typed extractors (class-validator, then zod, then a generic HttpException fallback) before re-throwing the original exception. It resolves CLS statically, so createProfilerValidationPipe(...) needs no DI container. See How it works on the package page.