{"id":283,"date":"2026-06-18T18:28:07","date_gmt":"2026-06-18T23:28:07","guid":{"rendered":"https:\/\/jeffsantillan.com\/blog\/?p=283"},"modified":"2026-06-19T00:27:18","modified_gmt":"2026-06-19T05:27:18","slug":"nestjs-uso-de-validator-y-transformers","status":"publish","type":"post","link":"https:\/\/jeffsantillan.com\/blog\/nestjs-uso-de-validator-y-transformers\/","title":{"rendered":"Nestjs uso de Validator y Transformers"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Sin validaciones, un usuario puede enviar cualquier dato a tu API \u2014 un email inv\u00e1lido, una edad negativa, o campos vac\u00edos \u2014 y llegar\u00edan directo a la base de datos. <code>class-validator<\/code> y <code>class-transformer<\/code> resuelven esto antes de que el request llegue al servicio<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Descripci\u00f3n<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em><strong><span style=\"text-decoration: underline;\">class-transform <\/span><\/strong><\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Se encarga de cambiar al tipo correcto de dato.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En el servicio no se ven por que hacen su trabajo antes de que llegue el request ah\u00ed. El servicio recibe los datos ya validados y transformados.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Donde act\u00faan los transformers en el Param(&#8216;id&#8217;) del controlador.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>\/\/ Sin transform: true\n@Get(':id')\nfindOne(@Param('id') id: string) {  \/\/ id llega como STRING \"1\"\n  return this.usuarioService.findOne(+id); \/\/ necesitas el +id para convertir\n}\n\n\/\/ Con transform: true puedes hacer esto:\n@Get(':id')\nfindOne(@Param('id', ParseIntPipe) id: number) { \/\/ id ya llega como NUMBER\n  return this.usuarioService.findOne(id); \/\/ sin el +\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">El @body tambien convierte los tipos automaticamente<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>\/\/ Si el DTO tiene edad: number\n\/\/ y el cliente manda: { \"edad\": \"25\" }  \u2190 string\n\/\/ transform: true lo convierte a n\u00famero autom\u00e1ticamente antes de llegar al servicio<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Se coloco un transform explicito dentro del DTO create (CreateUsuarioDto)<\/p>\n\n\n\n<pre class=\"wp-block-code has-medium-font-size\"><code>@Type(() =&gt; Number)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em><span style=\"text-decoration: underline;\">class-validator<\/span><\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Valida que los datos sean los correctos.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em><span style=\"text-decoration: underline;\">ValidationPipe<\/span><\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Se encarga de orquestar ambos.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Vamos a utilizar el validator y transformers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Lo primero hay que instalar las dependencias.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>npm install class-validator class-transformer<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">voy a modificar el archivo principal main.ts importante los ValidationPipe y agregando unas lineas para su uso.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>import { ValidationPipe } from '@nestjs\/common'; \/\/ Validator\n\n  app.useGlobalPipes(new ValidationPipe({\n    whitelist: true, \/\/ Elimina los campos que no estan en los DTOs\n    forbidNonWhitelisted: true, \/\/ Lanza un error si llegan campos extra\n    transform: true,  \/\/ Transforma tipos automaticamente.\n  }));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Sin <\/strong>whitelist considera que el campo extra llega al servicio<br><strong>Con <\/strong>whitelist: true considera que campo extra se elimina silenciosamente<br><strong>Con <\/strong>forbidNonWhitelisted: true >> lanza error 400<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quedando de esta manera.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>import { NestFactory } from '@nestjs\/core';\nimport { AppModule } from '.\/app.module';\nimport { ValidationPipe } from '@nestjs\/common'; \/\/ Validator\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  app.useGlobalPipes(new ValidationPipe({\n    whitelist: true, \/\/ Elimina los campos que no estan en los DTOs\n    forbidNonWhitelisted: true, \/\/ Lanza un error si llegan campos extra\n    transform: true,  \/\/ Transforma tipos automaticamente.\n  }));\n\n  await app.listen(process.env.PORT ?? 3000);\n}\nbootstrap();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">He generado una entidad llamada Usuario para poder hacer las validaciones.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>nest g resource Usuario<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Entonces vamos a utilizar arrays en memoria dado que no me he conectado a una base de datos prisma.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>\/\/ Usaremos arrays en memoria, dado que no conecte a prisma\nexport class Usuario {\n    id!: number;\n    nombre!: string;\n    email!: string;\n    edad!: number;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">DTOs <strong>(Simple sin validaciones)<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>export class CreateUsuarioDto {\n    nombre!: string;\n    email!: string;\n    edad!: number;    \n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ahora le voy a incluir las validaciones en una primera version.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>import { IsEmail, IsNotEmpty, IsString, IsNumber, Min, Max } from 'class-validator';\n<em>import { Type } from 'class-transformer';<\/em>\n\nexport class CreateUsuarioDto {\n\n    @IsNotEmpty({message: 'El nombre es obligatorio'})\n    @IsString()\n    nombre!: string;\n\n    @IsNotEmpty()\n    @IsEmail({}, {message: 'Email inv\u00e1lido'})\n    email!: string;\n\n    @IsNumber()\n    @Min(18, {message: 'Debe de ser mayor de 18'})\n    @Max(99)\n    @Type( () =&gt; Number) \/\/Aqui se utiliza la clase transform explicitamente.\n    edad!: number;\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>import { PartialType } from '@nestjs\/mapped-types';\nimport { CreateUsuarioDto } from '.\/create-usuario.dto';\n\nexport class UpdateUsuarioDto extends PartialType(CreateUsuarioDto) {}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Se define el servicio.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>import { Injectable, NotFoundException } from '@nestjs\/common';\nimport { CreateUsuarioDto } from '.\/dto\/create-usuario.dto';\nimport { UpdateUsuarioDto } from '.\/dto\/update-usuario.dto';\nimport { Usuario } from '.\/entities\/usuario.entity'; \/\/ Importar entity\n\n@Injectable()\nexport class UsuarioService {\n\n  \/\/ Array en memoria como una base de datos temporal\n  private usuarios: Usuario&#91;] = &#91;]\n  private idCounter = 1;\n\n  create(data: CreateUsuarioDto): Usuario {\n    const usuario: Usuario = {\n      id: this.idCounter++,\n      ...data,\n    };\n    this.usuarios.push(usuario);\n    return usuario;\n  }\n\n  findAll(): Usuario&#91;] {\n    return this.usuarios;\n  }\n\n  findOne(id: number): Usuario {\n    const usuario = this.usuarios.find(u =&gt; u.id === id);\n    if (!usuario) throw new NotFoundException(`Usuario ${id} no encontrado`);\n    return usuario;\n  }\n\n  update(id: number, data: UpdateUsuarioDto): Usuario {\n    const usuario = this.findOne(id);\n    Object.assign(usuario, data) \/\/ Actualiza solo los campos enviados\n    return usuario;\n  }\n\n  remove(id: number): {message: string} {\n    const index = this.usuarios.findIndex(u =&gt; u.id ===id);\n    if (index === -1) throw new NotFoundException(`Usuario ${id} no encontrado`);\n    this.usuarios.splice(index, 1);\n    return {message: `Usuario ${id} eliminado`};\n  }\n  \n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En el controlador y el modulo, no hay mayor cambio.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Formas de validar la operaciones CRUD de Usuario.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code># Crear\nInvoke-WebRequest -Uri http:\/\/localhost:3000\/usuario -Method POST -Headers @{\"Content-Type\"=\"application\/json\"} -Body '{\"nombre\":\"Jeff\",\"email\":\"jeff@gmail.com\",\"edad\":25}' | Select-Object -ExpandProperty Content\n\n# Listar\nInvoke-WebRequest -Uri http:\/\/localhost:3000\/usuario -Method GET | Select-Object -ExpandProperty Content | ConvertFrom-Json\n\n# Actualizar\nInvoke-WebRequest -Uri http:\/\/localhost:3000\/usuario\/1 -Method PATCH -Headers @{\"Content-Type\"=\"application\/json\"} -Body '{\"nombre\":\"Jeff Santillan\"}' | Select-Object -ExpandProperty Content\n\n# Eliminar\nInvoke-WebRequest -Uri http:\/\/localhost:3000\/usuario\/1 -Method DELETE | Select-Object -ExpandProperty Content<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Validaciones<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Error por ingreso de edad superior a 99<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"234\" src=\"https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-44-1024x234.png\" alt=\"\" class=\"wp-image-286\" srcset=\"https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-44-1024x234.png 1024w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-44-300x69.png 300w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-44-768x176.png 768w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-44-1300x298.png 1300w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-44.png 1511w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Mensaje de error por que no se ingreso un nombre:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"215\" src=\"https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-45-1024x215.png\" alt=\"\" class=\"wp-image-287\" srcset=\"https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-45-1024x215.png 1024w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-45-300x63.png 300w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-45-768x161.png 768w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-45-1300x273.png 1300w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-45.png 1461w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Mensaje de error por un email no valido.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"166\" src=\"https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-46-1024x166.png\" alt=\"\" class=\"wp-image-288\" srcset=\"https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-46-1024x166.png 1024w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-46-300x49.png 300w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-46-768x124.png 768w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-46-1300x210.png 1300w, https:\/\/jeffsantillan.com\/blog\/wp-content\/uploads\/2026\/06\/image-46.png 1465w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Se logra la validaci\u00f3n de manera exitosa.<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>{\n  \"id\": 1,\n  \"nombre\": \"Jeff\",\n  \"email\": \"jeff@gmail.com\",\n  \"edad\": 25\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u00a1Saludos!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Sin validaciones, un usuario puede enviar cualquier dato a tu API \u2014 un email inv\u00e1lido, una &hellip; <a title=\"Nestjs uso de Validator y Transformers\" class=\"hm-read-more\" href=\"https:\/\/jeffsantillan.com\/blog\/nestjs-uso-de-validator-y-transformers\/\"><span class=\"screen-reader-text\">Nestjs uso de Validator y Transformers<\/span>Leer m\u00e1s<\/a><\/p>\n","protected":false},"author":2,"featured_media":211,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[48,2,52],"tags":[53,49,54],"class_list":["post-283","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-nestjs","category-programacion","category-rest-api","tag-api","tag-nestjs","tag-programacion"],"_links":{"self":[{"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/posts\/283","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/comments?post=283"}],"version-history":[{"count":10,"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/posts\/283\/revisions"}],"predecessor-version":[{"id":312,"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/posts\/283\/revisions\/312"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/media\/211"}],"wp:attachment":[{"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/media?parent=283"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/categories?post=283"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jeffsantillan.com\/blog\/wp-json\/wp\/v2\/tags?post=283"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}