En esta página

TypeORM y entidades — persistencia de datos

15 min lectura TextoCap. 3 — Datos y persistencia

TypeORM en el ecosistema NestJS

TypeORM es la ORM (Object-Relational Mapper) más popular del ecosistema NestJS. Permite trabajar con bases de datos relacionales como PostgreSQL, MySQL, SQLite y MariaDB usando TypeScript con tipado completo. La integración oficial @nestjs/typeorm hace que la configuración sea sencilla y el patrón Repository esté disponible mediante inyección de dependencias.

Instalación de dependencias

Para comenzar con TypeORM y PostgreSQL:

npm install @nestjs/typeorm typeorm pg

Para MySQL/MariaDB:

npm install @nestjs/typeorm typeorm mysql2

Para SQLite (desarrollo local sin servidor):

npm install @nestjs/typeorm typeorm better-sqlite3

Configuración de TypeOrmModule

Configuración síncrona (simple)

TypeOrmModule.forRoot({
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'postgres',
  password: 'secreto',
  database: 'mi_api',
  entities: [__dirname + '/**/*.entity{.ts,.js}'],
  synchronize: true, // ¡solo en desarrollo!
})

Configuración asíncrona con ConfigService (recomendada)

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    type: 'postgres' as const,
    host: config.getOrThrow<string>('DB_HOST'),
    port: config.getOrThrow<number>('DB_PORT'),
    username: config.getOrThrow<string>('DB_USER'),
    password: config.getOrThrow<string>('DB_PASS'),
    database: config.getOrThrow<string>('DB_NAME'),
    autoLoadEntities: true, // carga las entidades registradas con forFeature()
    synchronize: config.get('NODE_ENV') !== 'production',
  }),
})

autoLoadEntities: true es conveniente porque evita tener que listar manualmente todas las entidades —TypeORM las carga automáticamente cuando se registran con TypeOrmModule.forFeature().

Decoradores de entidades

@Entity

El decorador @Entity() marca una clase como entidad de TypeORM y acepta varias opciones:

@Entity({
  name: 'tb_productos',    // nombre personalizado de la tabla
  schema: 'inventario',    // esquema de base de datos
  orderBy: { nombre: 'ASC' }, // orden por defecto en consultas
})
export class Producto {}

@PrimaryGeneratedColumn

@PrimaryGeneratedColumn()           // auto-increment integer
@PrimaryGeneratedColumn('uuid')     // UUID v4
@PrimaryGeneratedColumn('rowid')    // rowid en SQLite

También puedes usar @PrimaryColumn() para claves primarias manuales o compuestas.

@Column y sus tipos

// Tipos básicos
@Column({ type: 'varchar', length: 255 })
nombre: string;

@Column({ type: 'text' })
descripcion: string;

@Column({ type: 'int' })
cantidad: number;

@Column({ type: 'decimal', precision: 10, scale: 2 })
precio: number;

@Column({ type: 'boolean', default: true })
activo: boolean;

// JSON/JSONB (PostgreSQL)
@Column({ type: 'jsonb', nullable: true })
metadatos: Record<string, unknown> | null;

// Arrays (PostgreSQL)
@Column({ type: 'text', array: true, default: [] })
etiquetas: string[];

// Enum
@Column({ type: 'enum', enum: EstadoProducto, default: EstadoProducto.ACTIVO })
estado: EstadoProducto;

Columnas de timestamp automáticas

@CreateDateColumn()       // se establece automáticamente al crear
creadoEn: Date;

@UpdateDateColumn()       // se actualiza automáticamente al modificar
actualizadoEn: Date;

@DeleteDateColumn()       // para soft-delete (null si no está borrado)
eliminadoEn: Date | null;

@VersionColumn()          // versión optimistic locking (auto-incrementa)
version: number;

El patrón Repository

TypeORM en NestJS usa el patrón Repository para abstraer el acceso a datos. El Repository<Entidad> es genérico y provee todos los métodos CRUD estándar:

// Métodos más comunes del Repository<T>
this.repo.find(options)           // buscar múltiples registros
this.repo.findOne(options)        // buscar un registro
this.repo.findOneOrFail(options)  // lanza error si no encuentra
this.repo.count(options)          // contar registros
this.repo.create(dto)             // crear instancia (sin guardar)
this.repo.save(entidad)           // guardar (insert o update)
this.repo.update(criteria, dto)   // actualización parcial
this.repo.delete(criteria)        // eliminación física
this.repo.softDelete(criteria)    // eliminación lógica (@DeleteDateColumn)
this.repo.restore(criteria)       // restaurar soft-deleted
this.repo.insert(dto)             // insert sin retornar la entidad
this.repo.upsert(dto, keys)       // insert o update según llaves

Opciones de find

const productos = await this.productoRepository.find({
  where: {
    estado: EstadoProducto.ACTIVO,
    categoria: 'electrónica',
  },
  select: ['id', 'nombre', 'precio'],    // columnas a seleccionar
  order: { precio: 'ASC' },             // ordenamiento
  skip: 0,                               // offset para paginación
  take: 10,                              // limit
  relations: ['categoria', 'imagenes'], // relaciones a cargar (JOIN)
  withDeleted: false,                   // incluir soft-deleted
});

Hooks del ciclo de vida de la entidad

TypeORM permite definir lógica que se ejecuta automáticamente en ciertos momentos:

@Entity()
export class Usuario {
  @Column()
  password: string;

  @BeforeInsert()
  async hashPasswordAntes() {
    this.password = await bcrypt.hash(this.password, 12);
  }

  @AfterLoad()
  computarDatosDerivados() {
    // ejecutado después de cargar desde la base de datos
  }

  @BeforeUpdate()
  actualizarFecha() {
    // ejecutado antes de save() si el registro ya existe
  }
}

Custom Repository (TypeORM v0.3+)

Para queries más complejos, extiende el Repository base:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class ProductosRepository {
  constructor(
    @InjectRepository(Producto)
    private readonly repo: Repository<Producto>,
  ) {}

  async buscarConFiltros(filtros: FiltrosProductoDto): Promise<[Producto[], number]> {
    const qb = this.repo.createQueryBuilder('producto');

    if (filtros.categoria) {
      qb.andWhere('producto.categoria = :categoria', { categoria: filtros.categoria });
    }

    if (filtros.precioMin !== undefined) {
      qb.andWhere('producto.precio >= :precioMin', { precioMin: filtros.precioMin });
    }

    if (filtros.precioMax !== undefined) {
      qb.andWhere('producto.precio <= :precioMax', { precioMax: filtros.precioMax });
    }

    if (filtros.buscar) {
      qb.andWhere('LOWER(producto.nombre) LIKE LOWER(:buscar)', {
        buscar: `%${filtros.buscar}%`,
      });
    }

    qb.orderBy(`producto.${filtros.ordenarPor ?? 'creadoEn'}`, filtros.direccion ?? 'DESC')
      .skip(filtros.pagina * filtros.limite)
      .take(filtros.limite);

    return qb.getManyAndCount();
  }
}

Con TypeORM y las entidades bien definidas, tienes una capa de acceso a datos tipada y robusta. En la siguiente lección añadiremos relaciones entre entidades y veremos cómo gestionar los cambios en la base de datos con migraciones.

synchronize: true solo en desarrollo
La opción `synchronize: true` de TypeORM sincroniza automáticamente el esquema de la base de datos con tus entidades cada vez que arranca la aplicación. Nunca la actives en producción, ya que puede borrar columnas y datos existentes. En producción siempre usa migraciones explícitas.
UUIDs como claves primarias en APIs REST
Usa `@PrimaryGeneratedColumn('uuid')` en lugar de `@PrimaryGeneratedColumn()` (auto-increment) para APIs REST públicas. Los UUIDs no revelan el volumen de datos de tu aplicación, son seguros para exponer en URLs y facilitan la sincronización en arquitecturas distribuidas.