José Mauricio Lemus Rodezno
Les dejo el enlace a la documentación sobre este tema: https://docs.nestjs.com/techniques/serialization
Andres Trujillo
Gracias José!
¿Qué es la serialización y cómo se aplica en las APIs?
La serialización se refiere a la técnica de transformar datos antes de que un controlador los envíe al servicio o a una respuesta. Esta técnica es ampliamente utilizada en las APIs, ya sean REST o GraphQL, permitiendo la manipulación de los datos para excluir o agregar información según la necesidad.
¿Cómo activar la serialización en un proyecto?
Para usar la técnica de serialización, primero debes modificar el archivo main.typescript. Desde la librería @nestjs/common, junto con ValidationPipe, debes incluir un interceptor llamado ClassSerializerInterceptor. Posteriormente, configura la aplicación para que utilice este interceptor de manera global:
app.useGlobalInterceptors( new ClassSerializerInterceptor(app.get(Reflector)) );
Este paso inicial configura el proyecto para que pueda aplicar serialización en cualquier parte de la aplicación.
¿Cómo excluir información sensible?
La exclusión de datos es crucial, especialmente para información sensible como contraseñas. Usando ClassTransformer, puedes excluir campos como CREATED_AT e UPDATED_AT de las respuestas. A continuación, un ejemplo de cómo hacerlo en una entidad:
import { Exclude } from 'class-transformer'; class OrderItemEntity { @Exclude() createdAt: Date; @Exclude() updatedAt: Date; }
Cuando el serializador está activo, estos campos se eliminan automáticamente de la respuesta, protegiendo así la información.
¿Cómo transformar o agregar nueva información?
Además de excluir datos, la serialización permite transformar los datos existentes o agregar nuevos campos con la ayuda del decorador @Expose. Por ejemplo, si necesitas un array de productos más limpio, como en el caso de una orden, puedes usar @Expose para crear un campo que procese y retorne la estructura deseada.
Ejemplo de transformación
import { Expose } from 'class-transformer'; class OrderEntity { private items: OrderItemEntity[]; @Expose() get products() { return this.items?.filter(item => !!item) .map(item => ({ ...item.product, quantity: item.quantity, })) || []; } }
Este bloque de código asegura que sólo los datos necesarios, como los productos y su cantidad, se expongan de manera organizada.
Consideraciones al calcular campos dinámicamente
La técnica de serialización permite calcular campos dinámicamente. Sin embargo, es importante tener en cuenta el impacto en el rendimiento, especialmente cuando se trata de grandes volúmenes de datos. El cálculo de un total, por ejemplo, puede realizarse dentro de Node.js, pero para datos masivos es preferible delegar esos cálculos a una base de datos para evitar problemas de rendimiento.
Cálculo de un total de orden
Aquí un ejemplo de cómo implementar un cálculo de campo dinámico dentro de una entidad:
@Expose() get total() { return this.items?.filter(item => !!item) .reduce((total, item) => total + item.product.price * item.quantity, 0) || 0; }
Esta implementación depende de buenos hábitos de codificación y de optimización constante para asegurar que la API funcione de manera eficiente sin sobrecargar el servidor.
La serialización es una técnica poderosa cuando se aplica correctamente, asegurando que sólo la información relevante salga a la luz mientras mantiene la integridad y seguridad de los datos sensibles. ¡Practica implementando serialización en tus proyectos para profesionalizar la manipulación de datos en tus APIs!
Serializar
Esto sirve para transformar la información antes de retórnala
// src/main.ts // Admitimos serialiacion app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
Excluir un campo
Solo debemos agregarle el decorador @Exclude en el campo que queremos excluir
// src\users\entities\order-item.entity.ts import { Exclude } from "class-transformer"; ... @Exclude() @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP', }) createAt: Date;
Transformar la informacion
// src\users\entities\order.entity.ts @Expose() get products() { if (this.items) { return this.items .filter((item) => !!item) .map((item) => ({ ...item.product, quantity: item.quantity, itemId: item.id, })); } return []; } @Expose() get total() { if (this.items) { return this.items .filter((item) => !!item) .reduce((total, item) => { const totalItem = item.product.price * item.quantity; return total + totalItem; }, 0); } return 0; }
José Mauricio Lemus Rodezno
Les dejo el enlace a la documentación sobre este tema: https://docs.nestjs.com/techniques/serialization
Andres Trujillo
Gracias José!
Serializar
Serializar es transformar la información que el usuario nos esta enviando antes de que nuestro controlador la retorne al servicio.
Esto nos permite hacer una serie de cosas como:
Para hacer esto, vamos a tener que ir a nuestro **main.ts** y enviar la siguiente configuración:
// importamos el reflector import { Reflector } from '@nestjs/core'; // importamos el interceptor import { ClassSerializerInterceptor } from '@nestjs/common'; async function bootstrap() { // ... // usamos el interceptor en la configuración global // añadimos una nueva instancia de la clase "ClassSerializerInterceptor" a la configuración global // al crear la clase, le debemos mandar el reflector, para esto lo obtenemos y lo enviamos app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); } bootstrap();
Con esto, ya tenemos la configuración para utilizar los interceptors en toda nuestra aplicación, ¿Pero como lo hacemos? Veamos un ejemplo:
Supongamos que deseamos excluir cuando fue creado o actualizada una orden, ya que para el cliente puede ser innecesario verlo pero para la base de datos es importante tener un registro, ¿Cómo cambiaríamos esto? Para esto vamos a nuestra entidad de **order-item** y hagamos lo siguiente:
**src\users\entities\order-item.entity.ts**:// de "class-transformer", nos vamos a tarer el decorador exclude // "class-transformer" es quien nos da los decoradores para serializar la información import { Exclude } from 'class-transformer'; @Entity({ name: 'order_items' }) export class OrderItem { // ... // excluímos el campo deseado cuando lo retornemos @Exclude() @CreateDateColumn({ name: 'created_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP', }) createdAt: Date; // excluímos el campo deseado cuando lo retornemos @Exclude() @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP', }) updatedAt: Date; }
Bien, ahora veamos como podemos modificar un dato antes de enviarlo. Digamos que deseamos transformar el Array de las ordenes antes de que lo enviemos, por ejemplo, si queremos hacer que el resultado de nuestra petición sea mucho más limpia podemos hacer que los items de la orden sean solo un array de productos y que nos dé el precio total de la orden de compra. Para realizar esto debemos utilizar el decorador **@Expose()**.
**src\users\entities\order.entity.ts**:// importamos decoradores import { Expose, Exclude } from 'class-transformer'; @Entity({ name: 'orders' }) export class Order { // ... // esto lo tenemos que excluír para que no duplique campos @Exclude() @OneToMany(() => OrderItem, (item) => item.order) items: OrderItem[]; // decorador para "crear nuevos datos" @Expose() get products() { // tenemos items? if (this.items) { // hacemos esta transformación return this.items .filter((items) => !!items) // nos aseguramos que no sea nulo o oundefined .map((item) => ({ // recorremos todos los items ...item, // le añadimos el campo "quantity" al producto quantity: item.quantity, // añadimos el identificador del item itemId: item.id, })); } return []; } // podemos crear un campo donde nos de el precio total de la orden @Expose() get total() { // tenemos items? if (this.items) { // hacemos esta transformación return this.items .filter((items) => !!items) // nos aseguramos que no sea nulo o oundefined .reduce((acum, item) => { // el precio total es igual a el precio por la cantidad const totalItem = item.product.price * item.quantity; // se lo sumamos al acum en cada iteración return acum + totalItem; }, 0); } return 0; } }
Y listo, ya sabes usar una de las técnicas más útiles que tiene una API para manipular la información que le retorna al usuario. Sin embargo tienes que tener cuidado como manejas esta información porque hacer demasiados cálculos puede afectar negativamente el rendimiento de la app.
johnatan ramos
You are the best !!! ;)
Juan Araque
Ami no me funciona el exclude, ya he probado muchas cosas hasta poner el UseInterceptor en cada endpoint pero nada
Nicolas Molina
ProfesorVerifica que hayas implementado el interceptor de modo global, así:
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
Otra cosa que puede pasar es que en tu método no estés retornando lo que te da TypeOrm, sino que antes haces alguna modificación eso hace que el Interceptor no se aplique porque ya no en una objeto de la entidad, ejemplo:
async findAll() { const rta = await this.orderRepo.find(); return rta.map(item => change) }
Santiago Restrepo
¡Hi comunity! Espero puedan ayudarme con esta pregunta...
Me gustaría saber qué approach se puede seguir con esta prácitca de serializar, más específicamente me gustaría saber cómo los nestjs devs suelen crear mappers para las salidas de sus diferentes endpoints, ya que acá solo usamos el endpoint de orders, pero qué pasaría si quisiera retornar las órdenes con otra estructura en otro endpoint, cuál sería el approach ideal para hacer este tipo de cosas
Alejandro Valenzuela
Buenas, mi consulta viene con el tema de rendimiento, el profe dijo que los campos calculados generan perdida de rendimiento y eso tiene lógica, pero la parte de transformación de la información también genera rendimiento bajo ? Gracias!
David Hilera
¿Qué pasa si quiero eliminar solo un producto y no todo el item?
Robert Jarod Meza Garcia
Tengo una pregunta, deseaba tener una conexión con mongo y tambien con postgres en un mismo proyecto nestjs y noté que el problema que se tiene con mongoose al implementar ClassSerializerInterceptor y Reflector, los métodos con mongoose devuelven un objeto completo del modelo de Esa librería y cuando se intenta guardar da un error 500.
Cuando se comenta esta especie de middleware funciona normalmente
Miguel Angel Reyes Moreno
Uff, qué bello ese reduce :moyai: :smoking:
Naldo Duran
👏