NestJS e TypeORM em 2026: migrations, relações e perguntas de entrevista

Dominar a integração do NestJS com o TypeORM usando as migrations do TypeORM 1.0, as relações entre entidades, o padrão repository e as perguntas de entrevista comuns para desenvolvedores backend.

Diagrama de arquitetura de migrations e relações do NestJS e TypeORM

A integração do NestJS com o TypeORM continua sendo uma das mais testadas em produção para backends Node.js corporativos. Com o TypeORM chegando à versão 1.0 em maio de 2026, depois de quase uma década de desenvolvimento, e o NestJS 11 entregando o Express v5 por padrão, a stack amadureceu e virou uma base estável para aplicações em produção. Este guia cobre os padrões essenciais que todo desenvolvedor backend precisa dominar: migrations, relações entre entidades, o padrão repository e as perguntas de entrevista que vêm junto.

O marco do TypeORM 1.0

O TypeORM 1.0.0, lançado em 19 de maio de 2026, moderniza os requisitos da plataforma para ECMAScript 2023, remove as APIs descontinuadas e corrige a ordem das migrations: as migrations pendentes agora rodam antes da sincronização do esquema quando ambas estão habilitadas.

Configurar as migrations do TypeORM no NestJS 11

Primeira regra dos bancos de dados em produção: nunca usar synchronize: true. A sincronização automática compara os metadados das entidades com o esquema ativo e aplica as mudanças diretamente, o que arrisca perda de dados ao remover colunas, mudar tipos ou renomear campos. As migrations oferecem uma alternativa versionada, revisável e reversível.

O TypeORM 1.0 exige um arquivo de configuração DataSource dedicado para o CLI, separado da configuração do módulo do NestJS. O CLI não consegue resolver a injeção de dependências do NestJS, então precisa de um ponto de entrada independente.

src/config/typeorm.config.tstypescript
import { DataSource } from 'typeorm';
import { config } from 'dotenv';

config(); // Load .env variables

export default new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/migrations/*.ts'],
  synchronize: false,
});

A configuração do módulo do NestJS reflete esses ajustes, mas usa o padrão TypeOrmModule.forRootAsync com injeção de dependências:

src/app.module.tstypescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get('DB_HOST'),
        port: config.getOrThrow<number>('DB_PORT'),
        username: config.get('DB_USER'),
        password: config.get('DB_PASSWORD'),
        database: config.get('DB_NAME'),
        autoLoadEntities: true, // Auto-register entities from feature modules
        synchronize: false,     // Always false in production
      }),
    }),
  ],
})
export class AppModule {}

autoLoadEntities: true substitui o array manual entities ao registrar automaticamente cada entidade importada via TypeOrmModule.forFeature() nos módulos de funcionalidade.

O fluxo de migration: gerar, revisar, executar

O TypeORM 1.0 entrega o typeorm-ts-node-commonjs como o runner de CLI recomendado, substituindo a abordagem antiga ts-node ./node_modules/typeorm/cli. Adicionar estes scripts ao package.json:

package.json (scripts section)json
{
  "migration:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/$npm_config_name -d src/config/typeorm.config.ts",
  "migration:run": "typeorm-ts-node-commonjs migration:run -d src/config/typeorm.config.ts",
  "migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/config/typeorm.config.ts"
}

O comando de geração compara os metadados das entidades com o banco de dados ativo e produz instruções SQL. Sempre revisar a saída antes de fazer o commit. O TypeORM detecta adições de colunas, remoções, mudanças de tipo, alterações de índices e mudanças em chaves estrangeiras, mas deixa passar os backfills de dados, os renomeios de tabelas (que aparecem como um drop seguido de uma criação) e qualquer coisa que envolva extensões ou triggers do banco.

Revisar cada migration gerada

Uma coluna renomeada gera um DROP + ADD em vez de um ALTER RENAME. Isso destrói os dados existentes. Sempre inspecionar o SQL no método up() e ajustar manualmente quando necessário.

Relações entre entidades: OneToMany, ManyToOne, ManyToMany

As relações entre entidades do TypeORM se traduzem diretamente em chaves estrangeiras SQL por meio de decorators. Entender os três tipos de relação principais e suas opções é fundamental para qualquer módulo de banco de dados no NestJS.

Um User com vários registros Order ilustra o padrão mais comum:

src/entities/user.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm';
import { Order } from './order.entity';

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

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

  @Column()
  name: string;

  @OneToMany(() => Order, (order) => order.user)
  orders: Order[];  // No DB column created here; relation lives on the Order side

  @CreateDateColumn()
  createdAt: Date;
}
src/entities/order.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';

@Entity('orders')
export class Order {
  @PrimaryGeneratedColumn('uuid')
  id: string;

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

  @Column({ default: 'pending' })
  status: string;

  @ManyToOne(() => User, (user) => user.orders, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })  // Explicit FK column name
  user: User;
}

O lado @ManyToOne é dono da chave estrangeira. @JoinColumn é opcional no @ManyToOne (o TypeORM o infere), mas um nome explícito evita surpresas quando o nome da coluna importa para consultas cruas ou diffs de migration.

Para as relações many-to-many, o TypeORM cria uma tabela de junção automaticamente:

src/entities/product.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Category } from './category.entity';

@Entity('products')
export class Product {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;

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

  @ManyToMany(() => Category, (category) => category.products)
  @JoinTable({ name: 'product_categories' })  // Owns the junction table
  categories: Category[];
}

@JoinTable precisa aparecer em exatamente um lado da relação. Ele define qual entidade é dona da tabela de junção e controla o nome dela.

Pronto para mandar bem nas entrevistas de Node.js / NestJS?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

O padrão repository e os repositories personalizados

O sistema de módulos e injeção de dependências do NestJS integra os repositories do TypeORM via TypeOrmModule.forFeature(). Injetar o Repository<T> padrão cobre as operações CRUD comuns. Quando as consultas ficam complexas, os repositories personalizados encapsulam essa lógica.

src/orders/orders.module.tstypescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from '../entities/order.entity';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';

@Module({
  imports: [TypeOrmModule.forFeature([Order])],
  providers: [OrdersService],
  controllers: [OrdersController],
})
export class OrdersModule {}
src/orders/orders.service.tstypescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '../entities/order.entity';

@Injectable()
export class OrdersService {
  constructor(
    @InjectRepository(Order)
    private readonly orderRepo: Repository<Order>,
  ) {}

  async findByUser(userId: string): Promise<Order[]> {
    return this.orderRepo.find({
      where: { user: { id: userId } },
      relations: ['user'],          // Eager-load the user relation
      order: { createdAt: 'DESC' }, // Most recent first
    });
  }

  async findWithFilters(status: string, minTotal: number): Promise<Order[]> {
    // QueryBuilder for complex queries
    return this.orderRepo
      .createQueryBuilder('order')
      .leftJoinAndSelect('order.user', 'user')
      .where('order.status = :status', { status })
      .andWhere('order.total >= :minTotal', { minTotal })
      .orderBy('order.total', 'DESC')
      .getMany();
  }
}

A API find() resolve as consultas simples. O QueryBuilder resolve os joins, as subconsultas, as agregações e tudo que exige um controle fino do SQL. Combinar os dois no mesmo serviço é perfeitamente válido: usar find() para buscas simples e QueryBuilder quando a consulta não puder ser expressa pela API de opções.

Gestão de transações para a integridade dos dados

As operações que modificam várias tabelas precisam de transações. O TypeORM oferece duas abordagens: o método DataSource.transaction() e a API QueryRunner.

src/orders/orders.service.ts (additional method)typescript
async createOrderWithItems(
  userId: string,
  items: { productId: string; quantity: number }[],
  dataSource: DataSource,
): Promise<Order> {
  return dataSource.transaction(async (manager) => {
    // All operations use the transactional manager
    const order = manager.create(Order, {
      user: { id: userId },
      total: 0,
      status: 'pending',
    });
    const savedOrder = await manager.save(order);

    let total = 0;
    for (const item of items) {
      const product = await manager.findOneByOrFail(Product, { id: item.productId });
      total += product.price * item.quantity;

      await manager.save(OrderItem, {
        order: savedOrder,
        product,
        quantity: item.quantity,
        unitPrice: product.price,
      });
    }

    savedOrder.total = total;
    return manager.save(savedOrder);
  });
}

O método transaction() baseado em callback faz commit automaticamente em caso de sucesso e faz rollback diante de qualquer erro lançado. Cada operação de banco de dados dentro do callback precisa usar o manager fornecido em vez do repository injetado, caso contrário essas operações rodam fora da transação.

QueryRunner ou transaction()?

Usar DataSource.transaction() para as operações de múltiplas etapas mais simples. Migrar para o QueryRunner quando for necessário controle manual sobre os pontos de commit/rollback ou sobre os savepoints, como nos rollbacks parciais em processamento por lotes.

Perguntas de entrevista comuns sobre NestJS e TypeORM

Essas perguntas aparecem com frequência nas entrevistas de desenvolvedores backend. Cada resposta mira a profundidade esperada em uma discussão técnica de nível sênior.

Por que synchronize: true nunca deve ser usado em produção?

A sincronização automática aplica mudanças de esquema destrutivas sem confirmação. Remover uma coluna, mudar um tipo ou tirar um índice acontece de imediato sobre dados em produção. As migrations trazem controle de versão, revisão de código e capacidade de rollback. O único uso seguro do synchronize é o desenvolvimento local com dados descartáveis.

Qual é a diferença entre as relações eager e lazy?

As relações eager são carregadas automaticamente em cada consulta sobre a entidade pai. As relações lazy retornam uma Promise e executam uma consulta separada apenas quando acessadas. O carregamento eager gera uma sobrecarga do tipo N+1 quando os dados relacionados não são necessários. O carregamento lazy adia o custo, mas pode disparar consultas inesperadas no fundo da pilha de chamadas. A opção explícita relations no find() ou leftJoinAndSelect no QueryBuilder dá controle total sobre o que é carregado por consulta.

Como o cascade funciona nas relações do TypeORM?

Definir cascade: true em uma relação significa que salvar a entidade pai também persiste as entidades filhas não salvas. cascade: ['insert', 'update'] limita o comportamento a operações específicas. Os cascades operam no nível do ORM, não no nível do banco de dados. Para exclusões em cascata no nível do banco de dados, usar onDelete: 'CASCADE' no decorator @ManyToOne, que gera uma restrição ON DELETE CASCADE na migration.

Quando um repository personalizado deve ser usado em vez do Repository<T> padrão?

Os repositories personalizados encapsulam lógica de consulta complexa que não pertence aos serviços. Sinais para extrair um repository personalizado: a mesma cadeia de QueryBuilder aparece em vários serviços, a consulta envolve vários joins ou subconsultas, ou a consulta exige SQL cru. Chamadas simples de find() não justificam um repository personalizado.

Para mais perguntas sobre a arquitetura do NestJS, ver o guia completo sobre guards e interceptors no NestJS.

Pronto para mandar bem nas entrevistas de Node.js / NestJS?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Conclusão

  • Usar um arquivo de configuração DataSource separado para o CLI do TypeORM, independente da configuração do módulo do NestJS
  • Nunca habilitar synchronize em ambientes de produção; apoiar-se em migrations geradas e revisadas para todas as mudanças de esquema
  • Colocar @ManyToOne na entidade que é dona da chave estrangeira, e usar um nome explícito com @JoinColumn para mais clareza nas consultas cruas e na saída das migrations
  • Escolher entre a API de opções find() para consultas simples e o QueryBuilder para joins complexos, agregações e lógica condicional
  • Envolver as mutações multitabela em transações com DataSource.transaction(), e garantir que cada operação dentro do callback use o manager transacional
  • Com o TypeORM 1.0 estabilizando a superfície da sua API e o NestJS 11 amadurecendo sua arquitetura de módulos, essa stack continua sendo uma escolha sólida para backends corporativos que exigem um acesso a dados estruturado e tipado

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#nestjs
#typeorm
#migrations
#database
#typescript
#backend

Compartilhar

Artigos relacionados