TypeScript ORM

TypeORM

TypeORM mapea clases de TypeScript a tablas de base de datos mediante decoradores. Soporta los patrones Data Mapper y Active Record, e incluye repositorios, un QueryBuilder y migraciones en un solo paquete.

intermediate15 min readUpdated 16 sept 2026
user.entity.ts
ts
// user.entity.ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  OneToMany,
  CreateDateColumn,
} from "typeorm";
import { Post } from "./post.entity";

@Entity("users")
export class User {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column({ unique: true })
  email!: string;

  @Column({ name: "display_name" })
  displayName!: string;

  @CreateDateColumn({ name: "created_at" })
  createdAt!: Date;

  @OneToMany(() => Post, (post) => post.author)
  posts!: Post[];
}
Lanzado
2016
Lenguaje
TypeScript / JavaScript
Patrones
Data Mapper y Active Record
Bases de datos
Postgres, MySQL, SQLite, MSSQL, Oracle
Estilo
Entidades basadas en decoradores
Versión actual
0.3.x

Por que importa

Lo que TypeORM aporta a un backend de TypeScript

Entidades impulsadas por decoradores

Las clases y los decoradores de propiedades describen tablas y columnas, por lo que el esquema reside en código tipado que el compilador puede verificar.

Relaciones de primera clase

@ManyToOne, @OneToMany y @ManyToMany mapean claves foráneas y tablas intermedias, con carga eager o explícita según tus necesidades.

Migraciones desde entidades

Genera archivos de migración comparando las entidades con la base de datos, y luego ejecútalos en orden en cada entorno.

La imagen completa

Tres ideas que definen cada app de TypeORM

Las entidades describen las tablas, un DataSource gestiona la conexión, y los repositorios o el QueryBuilder leen y escriben filas.

Entidades

Modelo

Una clase decorada se mapea a una tabla, y cada propiedad decorada se mapea a una columna o una relación.

DataSource

Conexión

Un único objeto contiene las opciones de conexión y crea gestores de entidades, repositorios y transacciones.

Repositorios

Consulta

Los repositorios tipados ofrecen find, findOne y save, mientras que el QueryBuilder cubre cualquier cosa que ellos no puedan expresar.

HTML5 de un vistazo

El kit de herramientas de TypeORM

Decoradores de columna

@Column, @PrimaryGeneratedColumn, @CreateDateColumn y similares definen la estructura de la tabla.

Relaciones

@ManyToOne, @OneToMany, @OneToOne y @ManyToMany conectan las tablas entre sí.

API de Repositorio

find, findOne, save, remove, count y findAndCount con cláusulas where tipadas.

QueryBuilder

Encadena select, join, where y orderBy, o recurre a SQL puro cuando sea necesario.

Migraciones

migration:generate escribe SQL a partir de las diferencias de las entidades; migration:run las aplica.

Transacciones

dataSource.transaction y QueryRunner envuelven escrituras de múltiples pasos de forma atómica.

Modelo de datos

Un usuario y sus publicaciones

La entidad User posee muchas filas de Post, y cada Post apunta de vuelta a un autor a través de una clave foránea.

Las tablas de usuarios y publicacionesEntidades de TypeORM
  • iduuidClave primaria generada, expuesta como string en TypeScript
  • emailtextColumna única, forzada por una restricción de base de datos
  • displayNametextMapeada a la columna display_name
  • createdAttimestamptzEstablecida automáticamente por @CreateDateColumn
  • postsPost[]Relación uno-a-muchos, cargada solo cuando se solicita

La entidad User posee muchas filas de Post, y cada Post apunta de vuelta a un autor a través de una clave foránea.

Una breve historia

De los decoradores al DataSource

  1. 2016

    Lanzamiento de TypeORM

    Llega un ORM basado en decoradores para TypeScript, con el objetivo de sentirse natural en una base de código tipada.

    16
  2. 2018

    Los años de NestJS

    @nestjs/typeorm convierte a TypeORM en la capa de datos predeterminada para una generación de aplicaciones Nest.

    18
  3. 2020

    Madurez de migraciones y suscriptores

    La comparación de esquemas, los suscriptores de entidades y los listeners de ciclo de vida completan el framework.

    20
  4. 2022

    Versión 0.3 y el DataSource

    createConnection es reemplazado por un DataSource explícito que posee la conexión y sus opciones.

    22
  5. 2024

    Un ORM maduro y ampliamente desplegado

    TypeORM sigue siendo común en backends de NestJS, incluso mientras Drizzle y Prisma atraen nuevos proyectos.

    24

La guia completa

TypeORM: Todo lo que necesitas saber

¿Qué es TypeORM?

TypeORM es un object-relational mapper para TypeScript y JavaScript. Te permite describir tu base de datos como un conjunto de clases decoradas con metadatos y, posteriormente, realizar consultas a través de repositorios o un constructor fluido. Existe desde 2016 y es el ORM más asociado con NestJS.

Su característica distintiva es que soporta dos patrones muy conocidos simultáneamente. En Active Record, una entidad sabe cómo cargarse y guardarse a sí misma. En Data Mapper, las entidades son objetos simples y un repositorio independiente se encarga de la persistencia. La mayoría de los equipos utilizan Data Mapper para el código de la aplicación y, ocasionalmente, Active Record para modelos pequeños y autónomos.

Si ya conoces SQL, la mejor forma de entender TypeORM es como una capa de mapeo: las entidades se convierten en tablas, los decoradores en definiciones de columnas y cada consulta termina siendo un SQL que podrías haber escrito a mano.

Entidades: clases que se convierten en tablas

Una entidad es una clase marcada con @Entity(). Cada instancia es una fila y cada propiedad decorada es una columna.

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity("users")
export class User {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column({ unique: true })
  email!: string;

  @Column({ name: "display_name" })
  displayName!: string;
}

El argumento de @Entity es el nombre de la tabla. Si lo omites, TypeORM deriva uno a partir del nombre de la clase, pero ser explícito evita sorpresas cuando se refactorizan los nombres. La opción name en @Column desacopla la columna de la base de datos de la propiedad de TypeScript, que es como displayName se mapea a display_name.

El ! después de cada propiedad es la aserción de asignación definitiva. TypeScript no puede detectar que el ORM rellena estos campos en tiempo de ejecución, por lo que ! le indica al compilador que confíe en la base de datos.

Decoradores de columna que realmente usarás

TypeORM infiere el tipo de columna a partir del tipo de TypeScript de la propiedad, pero la inferencia es limitada. Un string podría ser text, varchar o char, por lo que para cualquier cosa precisa, pasa el tipo explícitamente.

@Entity("posts")
export class Post {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ type: "text" })
  body!: string;

  @Column({ type: "integer", default: 0 })
  views!: number;

  @Column({ type: "boolean", default: false })
  published!: boolean;

  @CreateDateColumn({ name: "created_at" })
  createdAt!: Date;

  @UpdateDateColumn({ name: "updated_at" })
  updatedAt!: Date;
}

Vale la pena memorizar los decoradores de columnas generadas: @CreateDateColumn establece una marca de tiempo al insertar, @UpdateDateColumn la actualiza en cada modificación y @DeleteDateColumn habilita los borrados lógicos (soft deletes). @PrimaryGeneratedColumn("uuid") genera un UUID en lugar de un entero autoincremental, lo cual es útil cuando los IDs se exponen públicamente.

Para el dinero, almacena las unidades menores como enteros en una columna integer o bigint y nunca uses un float. Para atributos que sean genuinamente opcionales, una columna jsonb con { type: "jsonb" } está bien, pero cualquier cosa que necesites filtrar o restringir debe ir en su propia columna.

El DataSource y la configuración

Desde la versión 0.3, un DataSource es el objeto único que contiene las opciones de conexión y distribuye los repositorios, managers y transacciones.

import "reflect-metadata";
import { DataSource } from "typeorm";

export const AppDataSource = new DataSource({
  type: "postgres",
  url: process.env.DATABASE_URL,
  entities: ["src/entities/*.entity.ts"],
  migrations: ["src/migrations/*.ts"],
  synchronize: false,
  logging: ["error", "warn"],
});

La importación de reflect-metadata debe realizarse antes de cargar cualquier entidad, ya que los decoradores dependen de ella. En una compilación final, apunta entities y migrations a los archivos .js generados en lugar de las fuentes .ts.

Mantén synchronize desactivado en todas partes, excepto en una base de datos local temporal. Esta opción reescribe el esquema para que coincida con tus entidades al iniciar, lo que significa que una propiedad renombrada puede eliminar silenciosamente una columna y sus datos. Las migraciones existen precisamente para que los cambios de esquema sean revisados, versionados y repetibles.

Repositorios y la API find

Un repositorio es un gateway tipado para una entidad. Obtienes uno desde DataSource y lo utilizas para la mayoría de tus lecturas y escrituras.

const users = AppDataSource.getRepository(User);

const user = await users.findOneBy({ id });
const recent = await users.find({
  where: { email: ILike("%@example.com") },
  order: { createdAt: "DESC" },
  take: 20,
  skip: 0,
});

find devuelve un array y findOne devuelve una sola fila o null. El objeto de opciones acepta where, order, take, skip, select, relations y withDeleted. Los operadores como ILike, In, MoreThan y IsNull se encuentran en el paquete typeorm y se componen dentro de where.

Las escrituras utilizan create, save y remove:

const user = users.create({ email, displayName });
await users.save(user);

user.displayName = "Ada Lovelace";
await users.save(user);

save realiza un upsert: inserta cuando la clave primaria está ausente y actualiza cuando está presente. Esa conveniencia oculta si una fila fue creada o modificada, por lo que cuando la distinción sea importante, utiliza insert y update directamente.

Relaciones y cómo se cargan

Las relaciones se declaran en ambos lados. El lado @ManyToOne posee la foreign key, y @OneToMany es su inverso.

@Entity("posts")
export class Post {
  @ManyToOne(() => User, (user) => user.posts, { onDelete: "CASCADE" })
  @JoinColumn({ name: "author_id" })
  author!: User;

  @Column({ name: "author_id" })
  authorId!: string;
}

@Entity("users")
export class User {
  @OneToMany(() => Post, (post) => post.author)
  posts!: Post[];
}

Mantener una columna authorId explícita junto a la relación es un patrón común y útil: permite leer y filtrar por la foreign key sin necesidad de cargar la fila relacionada.

@ManyToMany requiere una tabla de unión, declarada con @JoinTable en el lado propietario. @OneToOne funciona como @ManyToOne pero impone unicidad.

La carga es la parte fundamental. Por defecto, las relaciones no se cargan. Debes solicitarlo mediante la opción relations, un flag de eager loading o un join de QueryBuilder.

const posts = await AppDataSource.getRepository(Post).find({
  relations: { author: true },
  where: { published: true },
});

Las relaciones eager se cargan automáticamente en cada find, y las relaciones lazy devuelven una promesa que dispara una consulta al acceder a ellas. Ambas son convenientes, pero ambas ocultan el número de consultas. Es preferible la carga explícita, especialmente en rutas críticas (hot paths), para que el coste de una consulta sea visible donde se escribe.

El QueryBuilder para consultas reales

El QueryBuilder cubre joins, agregados y filtros dinámicos que la API de find no puede expresar de manera limpia.

const posts = await AppDataSource.getRepository(Post)
  .createQueryBuilder("post")
  .innerJoinAndSelect("post.author", "author")
  .where("author.email = :email", { email })
  .andWhere("post.createdAt >= :since", { since })
  .orderBy("post.createdAt", "DESC")
  .take(20)
  .getMany();

Los alias son obligatorios y los parámetros siempre se vinculan con marcadores de posición :name, nunca mediante interpolación de strings. getMany devuelve entidades, getRawMany devuelve filas simples, y getOne o getManyAndCount cubren los casos comunes de fila única y paginación.

Los filtros dinámicos se construyen de forma natural:

const qb = repo.createQueryBuilder("post").where("post.published = true");

if (tag) {
  qb.andWhere("post.tags @> :tag", { tag: [tag] });
}
if (search) {
  qb.andWhere("post.title ILIKE :search", { search: `%${search}%` });
}

Cuando una consulta es demasiado especializada para el builder, dataSource.query() ejecuta SQL parametrizado directamente y devuelve filas raw.

Migraciones: generación y ejecución

Las migraciones son la única forma segura de modificar un esquema en producción. TypeORM puede generarlas comparando tus entidades con la base de datos activa.

npx typeorm migration:generate src/migrations/CreateUsers -d src/data-source.ts
npx typeorm migration:run -d src/data-source.ts
npx typeorm migration:revert -d src/data-source.ts

El archivo generado contiene los métodos up y down. Léelos antes de hacer el commit: el diff es una estimación y, ocasionalmente, puede generar sentencias destructivas que no tenías previstas.

import { MigrationInterface, QueryRunner } from "typeorm";

export class CreateUsers1710000000000 implements MigrationInterface {
  async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE users (
        id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
        email        text NOT NULL UNIQUE,
        display_name text NOT NULL,
        created_at   timestamptz NOT NULL DEFAULT now()
      )
    `);
  }

  async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE users`);
  }
}

Ejecuta las migraciones como un paso de despliegue dedicado con un rol que pueda realizar DDL, independiente del rol que utiliza la aplicación en tiempo de ejecución. Nunca permitas que la aplicación altere su propio esquema al iniciar.

Transacciones

Una transacción agrupa sentencias para que todas tengan éxito o todas se reviertan (roll back). El helper DataSource.transaction gestiona el commit, el rollback y la conexión por ti.

await AppDataSource.transaction(async (manager) => {
  const user = await manager.save(User, { email, displayName });
  await manager.save(Post, { title, body, author: user });
});

Si el callback lanza una excepción, TypeORM realiza el rollback y vuelve a lanzar el error. Utiliza el manager proporcionado para cada sentencia dentro del bloque; un repositorio obtenido directamente desde el DataSource utiliza una conexión diferente y se ejecutaría fuera de la transacción.

Para un control más preciso, createQueryRunner expone startTransaction, commitTransaction y rollbackTransaction. Esto es útil cuando la transacción abarca código que no puede expresarse como un único callback, pero mantén dichas transacciones cortas: mantener una abierta durante una llamada HTTP bloquea el vacuum y provoca contención de bloqueos (lock contention).

Evitando el problema N+1

El problema N+1 es el error de rendimiento más común en el código que utiliza un ORM. Ocurre cuando cargas una lista de posts, luego iteras sobre ella y accedes a post.author, lo que genera una consulta para la lista más una consulta adicional por cada post.

// N+1: one query for posts, then one per author.
const posts = await repo.find();
for (const post of posts) {
  console.log(post.author.email);
}

La solución es cargar la relación por adelantado en una sola consulta:

const posts = await repo.find({ relations: { author: true } });

Cuando solo necesites un conteo, evita cargar los hijos por completo:

const users = await AppDataSource.getRepository(User)
  .createQueryBuilder("user")
  .loadRelationCountAndMap("user.postCount", "user.posts")
  .getMany();

Activa el registro de consultas (query logging) en desarrollo y revisa el log después de cargar una página. Si una solicitud ejecuta docenas de consultas casi idénticas, tienes un problema N+1 y la opción relations o un join es la cura.

TypeORM en NestJS

@nestjs/typeorm integra el ORM con la inyección de dependencias de Nest. El módulo raíz gestiona la conexión y los módulos de funcionalidades solicitan los repositorios.

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: "postgres",
      url: process.env.DATABASE_URL,
      autoLoadEntities: true,
      synchronize: false,
    }),
    TypeOrmModule.forFeature([User, Post]),
  ],
})
export class AppModule {}

Posteriormente, un servicio recibe su repositorio a través del constructor:

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly users: Repository<User>,
  ) {}

  findByEmail(email: string) {
    return this.users.findOneBy({ email });
  }
}

autoLoadEntities registra las entidades que fueron pasadas a forFeature, lo que evita que la configuración raíz tenga una lista extensa de imports. Mantén las migraciones en la CLI de Nest o en un script independiente en lugar de ejecutarlas durante el inicio de la aplicación.

¿Active Record o Data Mapper?

La elección depende principalmente de dónde resida la persistencia. Active Record es más conciso para modelos simples, ya que la entidad lleva sus propios métodos de consulta.

@Entity("users")
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  email!: string;
}

const user = await User.findOneBy({ id });

Data Mapper mantiene las entidades como datos puros y traslada toda la persistencia a los repositorios:

const users = AppDataSource.getRepository(User);
const user = await users.findOneBy({ id });

Active Record es conveniente en scripts y servicios pequeños, pero acopla el modelo al ORM y dificulta las pruebas. Data Mapper es la mejor opción por defecto para el código de la aplicación, y es lo que NestJS recomienda. Elige un único estilo por proyecto en lugar de mezclarlos, ya que la consistencia es más importante que la elección específica.

Mejores prácticas

  • Mantén synchronize desactivado y gestiona cada cambio de esquema mediante una migración revisada.
  • Carga las relaciones explícitamente con relations o un join de QueryBuilder, y activa el log de queries en desarrollo.
  • Usa @Column({ type: ... }) en cualquier caso donde el tipo inferido pueda ser incorrecto.
  • Mantén la columna de clave foránea de authorId junto a la relación para realizar filtrados más eficientes.
  • Prefiere repositorios Data Mapper para el código de la aplicación y reserva Active Record para scripts.
  • Usa dataSource.transaction y pasa siempre la transacción manager en las llamadas anidadas.
  • Vincula cada valor como un parámetro; nunca interpoles la entrada del usuario directamente en el SQL.
  • Añade índices a las columnas que utilices para filtrar y hacer joins, y confírmalos con EXPLAIN ANALYZE.
  • Ejecuta las migraciones con un rol DDL dedicado, separado del rol de ejecución de la aplicación.

Errores comunes

  • Dejar synchronize: true en una base de datos compartida o de producción.
  • Acceder a una relación lazy dentro de un bucle y provocar una tormenta de consultas N+1.
  • Olvidar la importación de reflect-metadata, lo que rompe los metadatos de los decoradores en tiempo de ejecución.
  • Usar el repositorio DataSource dentro de una transacción en lugar del manager proporcionado.
  • Asumir que la migración generada siempre es segura; puede eliminar columnas al renombrarlas.
  • Tipar una columna como string y dejar que TypeORM infiera varchar(255) de forma inesperada.
  • Almacenar dinero como float y perder precisión.
  • Tratar a findOne como si lanzara una excepción; devuelve null cuando no hay coincidencias.
  • Mezclar los patrones Active Record y Data Mapper en la misma entidad.

Próximos pasos

TypeORM es una opción muy cómoda si trabajas con NestJS, y comprenderlo facilita la evaluación de otras alternativas. Lee la guía de Prisma para conocer un ORM basado en esquemas con un cliente generado, o la de Drizzle ORM si prefieres una capa ligera que mantenga SQL como protagonista. Para profundizar en la base de datos, la guía de PostgreSQL cubre índices, transacciones y el planificador, mientras que la de NestJS muestra cómo estructurar los servicios que envuelven tus repositorios.

En la practica

Entidades, repositorios, QueryBuilder, transacciones

Las cuatro capas que usas a diario, desde la definición de la clase hasta una escritura atómica.

src/entities/post.entity.ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToOne,
  JoinColumn,
  CreateDateColumn,
} from "typeorm";
import { User } from "./user.entity";

@Entity("posts")
export class Post {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  title!: string;

  @Column({ type: "text" })
  body!: string;

  @ManyToOne(() => User, (user) => user.posts, {
    onDelete: "CASCADE",
  })
  @JoinColumn({ name: "author_id" })
  author!: User;

  @Column({ name: "author_id" })
  authorId!: string;

  @CreateDateColumn({ name: "created_at" })
  createdAt!: Date;
}

Métodos de repositorio vs QueryBuilder

Comienza con los repositorios porque están tipados y es difícil cometer errores. Recurre al QueryBuilder cuando una consulta necesite joins, agregaciones o filtros dinámicos.

Repositorio
const users = await repo.find({
  where: { email: "[email protected]" },
  order: { createdAt: "DESC" },
  take: 10,
});
QueryBuilder
const users = await repo
  .createQueryBuilder("user")
  .leftJoinAndSelect("user.posts", "post")
  .where("user.email = :email", { email })
  .orderBy("user.createdAt", "DESC")
  .take(10)
  .getMany();

Relaciones eager vs carga explícita

La carga eager es conveniente, pero se ejecuta en cada find y puede generar silenciosamente múltiples consultas. Las relaciones explícitas mantienen el coste visible en el punto de llamada.

Preferir
// Explicit: one clear extra join, chosen per query.
const posts = await repo.find({
  relations: { author: true },
  where: { status: "published" },
});
Evitar
// Eager on the entity: loads on every single find,
// including the ones that never need the author.
@ManyToOne(() => User, { eager: true })
author!: User;

Compromisos

¿Deberías elegir TypeORM para un proyecto nuevo?

TypeORM es capaz y familiar para los equipos de NestJS, pero el ecosistema ha avanzado en algunos aspectos. Sopesa la ergonomía frente a las alternativas.

Strengths

  • Familiar para equipos de NestJS

    @nestjs/typeorm integra los repositorios en el inyector de dependencias, por lo que las entidades y servicios se sienten como código nativo de Nest.

  • Ambos patrones ORM en una librería

    Puedes usar repositorios Data Mapper para la mayor parte del código y Active Record para modelos simples, sin adoptar una segunda herramienta.

  • Relaciones y migraciones robustas

    Cuatro tipos de relaciones, cascadas, suscriptores y migraciones por diferencia de esquema cubren una amplia gama de modelado relacional.

Trade-offs

  • Carga silenciosa de relaciones

    Las relaciones lazy y eager facilitan el disparo de consultas no intencionadas, que es como aparecen los problemas N+1 en producción.

  • El tipado puede ser impreciso

    Las cláusulas where de find y el QueryBuilder aceptan strings, por lo que algunos errores solo surgen en tiempo de ejecución y no en tiempo de compilación.

  • Mantenimiento constante, pero no rápido

    El desarrollo es más lento que en Prisma o Drizzle, y algunos issues abiertos pueden persistir. Revisa el repositorio antes de comprometerte a largo plazo.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender TypeORM?

Nuestro tutorial interactivo te guia a traves de TypeORM paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.