En esta página
TypeORM y entidades — persistencia de datos
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 pgPara MySQL/MariaDB:
npm install @nestjs/typeorm typeorm mysql2Para SQLite (desarrollo local sin servidor):
npm install @nestjs/typeorm typeorm better-sqlite3Configuració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 SQLiteTambié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 llavesOpciones 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.
Inicia sesión para guardar tu progreso