Curso de Backend con NestJS

Relaciones many-to-many con TypeORM y validación de arrays

Curso de Backend con NestJS

Contenido del curso

Fundamentos y Primer CRUD

Base de Datos y Persistencia con TypeORM

Relaciones many-to-many con TypeORM y validación de arrays

Resumen

Las relaciones muchos a muchos en TypeORM permiten conectar entidades como posts y categorías sin crear manualmente una tabla intermedia. Aquí aprendes a configurarlas con decoradores, manejar bidireccionalidad y validar arrays de IDs en NestJS para construir APIs robustas.

Cómo funciona una relación muchos a muchos en bases de datos relacionales

En una relación many-to-many, la conexión no vive dentro de una sola tabla, como sí ocurre con las relaciones uno a uno o uno a muchos. Lo que sucede a nivel de base de datos es que se genera una tabla terciaria que mapea los identificadores de ambas entidades.

Piensa en posts y categorías: un post puede pertenecer a varias categorías, y una categoría puede contener varios posts. Para resolverlo, la base de datos crea una tabla con dos columnas, normalmente post_id y category_id, donde se registra cada combinación válida.

¿Qué es una tabla terciaria en relaciones muchos a muchos? Es una tabla intermedia con dos columnas que almacenan los IDs de las entidades relacionadas. Cada fila representa un match entre un registro de la primera tabla y otro de la segunda.

Lo interesante de TypeORM es que tú no creas esa tabla manualmente. Con un par de decoradores, la librería se encarga de gestionarla por detrás [03:15].

Cómo se configura ManyToMany y JoinTable en TypeORM

La relación se declara dentro de una de las entidades involucradas, no en una tercera. Puedes ponerla en Post o en Category, pero el decorador @JoinTable solo debe estar en uno de los dos lados [04:20].

En la entidad de post se importa ManyToMany y JoinTable desde TypeORM, y se enlaza la entidad Category. La configuración detallada del JoinTable es donde está la magia:

  • El nombre de la tabla terciaria, por ejemplo post_categories, en singular para ambas partes.
  • El joinColumn, que define la columna del lado actual: post_id referenciando al id de posts.
  • El inverseJoinColumn, que define la columna del otro lado: category_id referenciando al id de categories.

Esa configuración explícita es muchísimo mejor que dejar los nombres por defecto, porque te da control total sobre cómo queda la arquitectura de tu base de datos.

Cómo hacer la relación bidireccional entre Post y Category

Si quieres que desde una categoría también puedas resolver sus posts, declaras un @ManyToMany en la entidad Category apuntando a Post, pero sin JoinTable. La bidireccionalidad se conecta así: en post defines post.categories y en category defines categories.posts [06:40].

Esto te permite consultar la relación desde cualquiera de los dos lados, lo cual es útil pero también peligroso si las relaciones son muy grandes.

Por qué synchronize true puede romper tu base de datos

Al tener synchronize: true activado en TypeORM, cualquier cambio en las entidades se ejecuta automáticamente sobre la base de datos. Suena cómodo para aprender y debuguear, pero en producción es problemático.

Un ejemplo concreto: al crear primero la tabla terciaria con un nombre como posts y luego renombrarla a post_categories, TypeORM dejó la tabla anterior huérfana y creó la nueva en paralelo [08:05]. Esa basura queda acumulándose en tu esquema.

¿Cuándo debo usar synchronize true en TypeORM? Solo en desarrollo temprano o para experimentar. En producción debes usar migrations, que te dan control versionado sobre los cambios de esquema.

Cómo validar y guardar relaciones desde el DTO

Para crear un post con categorías asociadas, el DTO debe aceptar un array opcional de IDs. Las validaciones clave son:

  • @IsOptional() para no obligar a enviar categorías al crear el artículo desde cero.
  • @IsArray() para garantizar que el campo sea un array.
  • @IsNumber({}, { each: true }) para validar que cada elemento del array sea un número, no un string ni un objeto [10:30].

El campo se llama categoryIds porque lo que se recibe son los identificadores, no las categorías completas.

Cómo mapear categoryIds al guardar el post

En el servicio, cuando guardas el post, debes transformar el array [1, 2, 3] en un array de objetos [{id: 1}, {id: 2}, {id: 3}]. Eso es lo que TypeORM espera para crear las relaciones en la tabla terciaria.

La transformación se hace con un map sobre categoryIds, devolviendo un objeto con la propiedad id por cada elemento. Así, al ejecutar el save, TypeORM identifica las categorías existentes y arma los matches correspondientes.

Qué decisiones de negocio tomar al cargar relaciones

No todas las relaciones deben cargarse siempre. Esa es una decisión de diseño que depende del volumen esperado de datos, no de la IA.

En este caso, cargar las categorías de un post tiene sentido porque un artículo suele tener entre 3 y 10 categorías, similar a los tags de DevTo [12:45]. En cambio, cargar todos los posts de una categoría podría implicar miles de registros, lo cual hace pesada la consulta.

¿Debo cargar siempre las relaciones muchos a muchos? No. Carga solo las relaciones cuyo volumen sea razonable. Si una categoría puede tener miles de posts, evita cargarla por defecto y usa paginación o queries específicas.

Esa lógica solo la conoces tú y tu equipo, porque entienden el negocio.

Qué pasa si envías un ID de categoría inexistente

Si intentas relacionar un post con una categoría que no existe, por ejemplo el ID 4 cuando solo hay tres categorías, TypeORM lanza un error y no crea ninguna relación, ni siquiera con los IDs válidos [16:20].

Lo ideal sería personalizar el mensaje de error para informar al usuario qué ID falló, o decidir si se relacionan solo los existentes y se reporta el resto. Esas validaciones extra forman parte del diseño de tu API.

Si la categoría existe, el endpoint crea las relaciones correctamente y devuelve el array de categorías asociadas al post recién creado. ¿Te animas a implementar el endpoint que liste posts por categoría? Cuéntame en los comentarios cómo lo resolverías.