TypeScript ORM

TypeORM

O TypeORM mapeia classes TypeScript para tabelas de banco de dados usando decorators. Ele suporta os padrões Data Mapper e Active Record, e entrega repositórios, um QueryBuilder e migrações em um único pacote.

intermediate15 min readUpdated 16 de set. de 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[];
}
Lançado
2016
Linguagem
TypeScript / JavaScript
Padrões
Data Mapper e Active Record
Bancos de Dados
Postgres, MySQL, SQLite, MSSQL, Oracle
Estilo
Entidades baseadas em decorators
Versão atual
0.3.x

Por que importa

O que o TypeORM traz para um backend TypeScript

Entidades orientadas a decorators

Classes e decorators de propriedade descrevem tabelas e colunas, fazendo com que o schema resida em código tipado que o compilador pode validar.

Relações de primeira classe

@ManyToOne, @OneToMany e @ManyToMany mapeiam chaves estrangeiras e tabelas de junção, com carregamento eager ou explícito conforme sua necessidade.

Migrações a partir de entidades

Gere arquivos de migração comparando as entidades com o banco de dados e execute-os em ordem em todos os ambientes.

O panorama completo

Três ideias que moldam todo app TypeORM

Entidades descrevem tabelas, um DataSource gerencia a conexão, e repositórios ou o QueryBuilder leem e escrevem linhas.

Entidades

Modelo

Uma classe decorada mapeia para uma tabela, e cada propriedade decorada mapeia para uma coluna ou relação.

DataSource

Conexão

Um único objeto detém as opções de conexão e cria entity managers, repositórios e transações.

Repositórios

Consulta

Repositórios tipados oferecem find, findOne e save, enquanto o QueryBuilder cobre qualquer coisa que eles não consigam expressar.

HTML5 de uma olhada

O toolkit do TypeORM

Decorators de coluna

@Column, @PrimaryGeneratedColumn, @CreateDateColumn e outros definem a estrutura da tabela.

Relações

@ManyToOne, @OneToMany, @OneToOne e @ManyToMany conectam as tabelas.

API de Repositório

find, findOne, save, remove, count e findAndCount com cláusulas where tipadas.

QueryBuilder

Encadeie select, join, where e orderBy, ou use SQL puro quando necessário.

Migrações

migration:generate escreve SQL a partir de diffs de entidades; migration:run as aplica.

Transações

dataSource.transaction e QueryRunner envolvem escritas de múltiplas etapas atomicamente.

Modelo de dados

Um usuário e seus posts

A entidade User possui muitas linhas de Post, e cada Post aponta de volta para um autor através de uma chave estrangeira.

Tabelas de usuários e postsEntidades TypeORM
  • iduuidChave primária gerada, exposta como string no TypeScript
  • emailtextColuna única, forçada por uma constraint do banco de dados
  • displayNametextMapeada para a coluna display_name
  • createdAttimestamptzDefinida automaticamente por @CreateDateColumn
  • postsPost[]Relação um-para-muitos, carregada apenas quando solicitada

A entidade User possui muitas linhas de Post, e cada Post aponta de volta para um autor através de uma chave estrangeira.

Uma breve historia

De decorators ao DataSource

  1. 2016

    Lançamento do TypeORM

    Surge um ORM baseado em decorators para TypeScript, visando parecer natural em uma base de código tipada.

    16
  2. 2018

    Os anos NestJS

    @nestjs/typeorm torna o TypeORM a camada de dados padrão para uma geração de aplicações Nest.

    18
  3. 2020

    Maturação de migrações e subscribers

    Diff de schema, entity subscribers e listeners de ciclo de vida completam o framework.

    20
  4. 2022

    Versão 0.3 e o DataSource

    createConnection é substituído por um DataSource explícito que detém a conexão e suas opções.

    22
  5. 2024

    Um ORM maduro e amplamente implantado

    O TypeORM continua comum em backends NestJS, mesmo com Drizzle e Prisma atraindo novos projetos.

    24

O guia completo

TypeORM: Tudo que voce precisa saber

O que é TypeORM?

TypeORM é um object-relational mapper para TypeScript e JavaScript. Ele permite que você descreva seu banco de dados como um conjunto de classes decoradas com metadados e, em seguida, realize consultas por meio de repositories ou de um builder fluente. Ele existe desde 2016 e é o ORM mais associado ao NestJS.

Seu diferencial é que ele suporta dois padrões conhecidos simultaneamente. No Active Record, uma entidade sabe como carregar e salvar a si mesma. No Data Mapper, as entidades são objetos simples e um repository separado detém a persistência. A maioria das equipes utiliza Data Mapper para o código da aplicação e, ocasionalmente, Active Record para modelos pequenos e autocontidos.

Se você já conhece SQL, a melhor forma de entender o TypeORM é como uma camada de mapeamento: entidades tornam-se tabelas, decorators tornam-se definições de colunas e cada consulta eventualmente se torna um SQL que você poderia ter escrito manualmente.

Entidades: classes que se tornam tabelas

Uma entidade é uma classe marcada com @Entity(). Cada instância é uma linha e cada propriedade decorada é uma coluna.

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;
}

O argumento para @Entity é o nome da tabela. Se você omiti-lo, o TypeORM derivará um a partir do nome da classe, mas ser explícito evita surpresas quando os nomes são refatorados. A opção name em @Column desacopla a coluna do banco de dados da propriedade TypeScript, que é como displayName é mapeado para display_name.

O ! após cada propriedade é a asserção de atribuição definitiva (definite assignment assertion). O TypeScript não consegue perceber que o ORM preenche esses campos em tempo de execução, então o ! diz ao compilador para confiar no banco de dados.

Decoradores de coluna que você realmente usará

O TypeORM infere o tipo da coluna a partir do tipo TypeScript da propriedade, mas a inferência é limitada. Um string poderia ser text, varchar ou char, portanto, para qualquer coisa precisa, passe o tipo explicitamente.

@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;
}

Os decoradores de colunas geradas valem a pena memorizar: @CreateDateColumn define um timestamp na inserção, @UpdateDateColumn o atualiza a cada update, e @DeleteDateColumn habilita soft deletes. @PrimaryGeneratedColumn("uuid") gera um UUID em vez de um inteiro auto-incremento, o que é útil quando os IDs são expostos publicamente.

Para dinheiro, armazene unidades menores como inteiros em uma coluna integer ou bigint e nunca use float. Para atributos genuinamente opcionais, uma coluna jsonb com { type: "jsonb" } resolve, mas qualquer coisa que você filtre ou restrinja deve estar em sua própria coluna.

O DataSource e a configuração

Desde a versão 0.3, um DataSource é o objeto único que armazena as opções de conexão e fornece repositórios, managers e transações.

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"],
});

A importação do reflect-metadata deve ocorrer antes de qualquer entidade ser carregada, pois os decorators dependem dela. Em um build compilado, aponte entities e migrations para os arquivos .js gerados em vez dos fontes .ts.

Mantenha o synchronize desativado em todos os lugares, exceto em um banco de dados local descartável. Ele reescreve o schema para corresponder às suas entidades na inicialização, o que significa que uma propriedade renomeada pode excluir silenciosamente uma coluna e seus dados. As migrations existem precisamente para que as alterações de schema sejam revisadas, versionadas e repetíveis.

Repositórios e a API find

Um repositório é um gateway tipado para uma entidade. Você obtém um através do DataSource e o utiliza para a maioria das suas leituras e escritas.

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 retorna um array e findOne retorna uma única linha ou null. O objeto de opções aceita where, order, take, skip, select, relations e withDeleted. Operadores como ILike, In, MoreThan e IsNull estão no pacote typeorm e são compostos dentro de where.

Escritas utilizam create, save e remove:

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

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

save realiza um upsert: ele insere quando a chave primária está ausente e atualiza quando ela está presente. Essa conveniência oculta se uma linha foi criada ou alterada, portanto, quando essa distinção for importante, utilize insert e update diretamente.

Relações e como elas são carregadas

As relações são declaradas em ambos os lados. O lado @ManyToOne detém a foreign key, e @OneToMany é o seu 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[];
}

Manter uma coluna authorId explícita ao lado da relação é um padrão comum e útil: você pode ler e filtrar pela foreign key sem carregar a linha relacionada.

@ManyToMany precisa de uma join table, declarada com @JoinTable no lado proprietário. @OneToOne funciona como @ManyToOne, mas impõe unicidade.

O carregamento é a parte que importa. Por padrão, as relações não são carregadas. Você deve solicitá-las com a opção relations, uma flag de eager loading ou um join via QueryBuilder.

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

Relações eager são carregadas automaticamente em cada find, e relações lazy retornam uma promise que dispara uma query ao serem acessadas. Ambas são convenientes e ambas ocultam a contagem de queries. Prefira o carregamento explícito, especialmente em caminhos críticos (hot paths), para que o custo de uma query seja visível onde ela é escrita.

O QueryBuilder para consultas reais

O QueryBuilder abrange joins, agregados e filtros dinâmicos que a API find não consegue expressar de forma limpa.

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();

Aliases são obrigatórios e os parâmetros são sempre vinculados com placeholders :name, nunca via interpolação de strings. getMany retorna entidades, getRawMany retorna linhas simples (plain rows), e getOne ou getManyAndCount cobrem os casos comuns de linha única e paginação.

Filtros dinâmicos são construídos naturalmente:

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}%` });
}

Quando uma consulta é especializada demais para o builder, dataSource.query() executa SQL parametrizado diretamente e retorna linhas brutas (raw rows).

Migrations: gerando e executando

Migrations são a única maneira segura de alterar um schema em produção. O TypeORM pode gerá-las comparando suas entities com o banco de dados ativo.

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

O arquivo gerado contém os métodos up e down. Leia-os antes de fazer o commit: o diff é uma estimativa e, ocasionalmente, pode produzir instruções destrutivas que você não pretendia.

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`);
  }
}

Execute as migrations como uma etapa de deploy dedicada, utilizando uma role que possa realizar DDL, separada da role que a aplicação utiliza em tempo de execução. Nunca permita que a aplicação altere seu próprio schema durante a inicialização.

Transações

Uma transação agrupa instruções para que todas tenham sucesso ou todas sofram rollback. O helper DataSource.transaction gerencia o commit, o rollback e a conexão para você.

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

Se o callback lançar um erro, o TypeORM realiza o rollback e relança a exceção. Use o manager fornecido para cada instrução dentro do bloco; um repositório obtido diretamente do DataSource utiliza uma conexão diferente e seria executado fora da transação.

Para um controle mais refinado, o createQueryRunner expõe startTransaction, commitTransaction e rollbackTransaction. Isso é útil quando a transação abrange códigos que não podem ser expressos como um único callback, mas mantenha tais transações curtas: manter uma transação aberta durante uma chamada HTTP bloqueia o vacuum e favorece a contenção de locks.

Evitando o problema N+1

O problema N+1 é o bug de performance mais comum em códigos que utilizam ORM. Você carrega uma lista de posts, percorre essa lista e acessa post.author, gerando uma query para a lista mais uma query para 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);
}

A solução é carregar a relação antecipadamente em uma única query:

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

Quando você precisar apenas de uma contagem, evite carregar os filhos:

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

Ative o log de queries em ambiente de desenvolvimento e analise o log após carregar uma página. Se uma requisição disparar dezenas de queries quase idênticas, você tem um problema N+1, e a opção relations ou um join é a cura.

TypeORM no NestJS

@nestjs/typeorm integra o ORM com a injeção de dependência do Nest. O módulo raiz detém a conexão, e os módulos de funcionalidade (feature modules) solicitam os repositórios.

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

Um serviço, então, recebe seu repositório através do construtor:

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

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

autoLoadEntities registra as entidades que foram passadas para forFeature, o que mantém a configuração raiz livre de uma longa lista de imports. Mantenha as migrations no Nest CLI ou em um script independente, em vez de executá-las na inicialização da aplicação.

Active Record ou Data Mapper?

A escolha resume-se principalmente a onde a persistência reside. O Active Record é mais conciso para modelos simples, pois a entidade carrega seus próprios métodos de consulta.

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

  @Column()
  email!: string;
}

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

O Data Mapper mantém as entidades como dados puros e move toda a persistência para repositórios:

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

O Active Record é conveniente em scripts e serviços pequenos, mas acopla o modelo ao ORM e torna os testes mais difíceis. O Data Mapper é a melhor escolha padrão para código de aplicação, e é o que o NestJS incentiva. Escolha um estilo por projeto em vez de misturá-los, pois a consistência importa mais do que a escolha específica.

Melhores práticas

  • Mantenha o synchronize desativado e gerencie cada alteração de schema com uma migration revisada.
  • Carregue as relações explicitamente com relations ou um join via QueryBuilder, e ative o log de queries em desenvolvimento.
  • Use @Column({ type: ... }) para qualquer situação em que o tipo inferido possa estar incorreto.
  • Mantenha a coluna de chave estrangeira authorId ao lado da relação para filtragens mais performáticas.
  • Prefira repositórios Data Mapper para o código da aplicação e reserve o Active Record para scripts.
  • Use dataSource.transaction e sempre passe a transação manager para chamadas aninhadas.
  • Vincule cada valor como um parâmetro; nunca interpole entradas de usuário diretamente no SQL.
  • Adicione índices para as colunas que você utiliza em filtros e joins, e confirme-os com EXPLAIN ANALYZE.
  • Execute as migrations com uma role de DDL dedicada, separada da role de runtime da aplicação.

Erros comuns

  • Deixar synchronize: true em um banco de dados compartilhado ou de produção.
  • Acessar uma relação lazy dentro de um loop e criar uma tempestade de queries N+1.
  • Esquecer o import de reflect-metadata, o que quebra os metadados dos decorators em tempo de execução.
  • Usar o repositório DataSource dentro de uma transação em vez do manager fornecido.
  • Assumir que a migration gerada é sempre segura; ela pode remover colunas ao renomeá-las.
  • Tipar uma coluna como string e deixar o TypeORM inferir varchar(255) inesperadamente.
  • Armazenar dinheiro como float e perder precisão.
  • Tratar findOne como se ele lançasse um erro; ele retorna null quando nada corresponde.
  • Misturar os padrões Active Record e Data Mapper na mesma entidade.

Próximos passos

O TypeORM é uma escolha confortável se você trabalha com NestJS, e entendê-lo torna mais fácil avaliar as alternativas. Leia o guia do Prisma para um ORM schema-first com cliente gerado, ou o do Drizzle ORM se você prefere uma camada fina que mantenha o SQL em destaque. Para se aprofundar no banco de dados em si, o guia de PostgreSQL aborda índices, transações e o planner, enquanto o de NestJS mostra como estruturar os services que envolvem seus repositories.

Na pratica

Entidades, repositórios, QueryBuilder, transações

As quatro camadas que você usa diariamente, desde a definição da classe até uma escrita 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 Repositório vs QueryBuilder

Comece com repositórios porque são tipados e difíceis de errar. Recorra ao QueryBuilder quando a consulta precisar de joins, agregados ou filtros dinâmicos.

Repositório
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();

Relações Eager vs carregamento explícito

O carregamento eager é conveniente, mas executa em todo find e pode gerar silenciosamente muitas consultas. Relações explícitas mantêm o custo visível no local da chamada.

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;

Trade-offs

Você deve escolher TypeORM para um novo projeto?

O TypeORM é capaz e familiar para equipes NestJS, mas o ecossistema evoluiu em alguns pontos. Pese a ergonomia contra as alternativas.

Strengths

  • Familiar para equipes NestJS

    @nestjs/typeorm integra repositórios no injetor de dependências, fazendo com que entidades e serviços pareçam código nativo do Nest.

  • Ambos os padrões ORM em uma biblioteca

    Você pode usar repositórios Data Mapper para a maior parte do código e Active Record para modelos simples, sem adotar uma segunda ferramenta.

  • Relações e migrações ricas

    Quatro tipos de relação, cascades, subscribers e migrações por diff de schema cobrem uma ampla gama de modelagem relacional.

Trade-offs

  • Carregamento silencioso de relações

    Relações lazy e eager facilitam o disparo de consultas não intencionais, que é como problemas de N+1 aparecem em produção.

  • Tipagem pode ser imprecisa

    Cláusulas where de find e o QueryBuilder aceitam strings, então alguns erros só surgem em tempo de execução, não em tempo de compilação.

  • Manutenção constante, mas não rápida

    O desenvolvimento é mais lento que no Prisma ou Drizzle, e issues abertas podem demorar. Verifique o repositório antes de se comprometer a longo prazo.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender TypeORM?

Nosso tutorial interativo te guia por TypeORM passo a passo — com quizzes e codigo real que voce pode executar no navegador.