Como usar NestJS com TypeORM: Guia Completo
Introdução
Se você trabalha com Node.js há algum tempo, provavelmente já enfrentou aquele cenário clássico: o projeto começa pequeno, com queries manuais, e em poucos meses vira uma sopa de SQL espalhada por dezenas de arquivos. Manter, testar e evoluir esse código se torna um pesadelo. É exatamente aqui que a combinação de NestJS com TypeORM entra como solução robusta e escalável.
NestJS oferece uma arquitetura opinada, baseada em módulos, injeção de dependências e decorators. TypeORM é um ORM maduro que permite mapear entidades TypeScript diretamente para tabelas do banco de dados. Quando usados juntos, eles formam uma base sólida para APIs REST, microsserviços e aplicações de média a alta complexidade.
Neste guia, vamos cobrir o ciclo completo: da instalação à destruição de recursos, passando por configuração, exemplos práticos, boas práticas, segurança e os erros que mais pegam desenvolvedores desprevenidos.
Por que usar NestJS com TypeORM juntos
A integração entre NestJS e TypeORM não é acidental. O pacote oficial @nestjs/typeorm foi criado especificamente para conectar os dois ecossistemas de forma idiomática. Veja os principais motivos para adotar essa combinação:
Injeção de dependências nativa
O TypeORM registra repositórios como providers do NestJS. Isso significa que você injeta repositórios diretamente nos seus services sem nenhum acoplamento manual. O container de IoC do NestJS resolve tudo automaticamente.
Suporte a múltiplos bancos de dados
TypeORM suporta PostgreSQL, MySQL, MariaDB, SQLite, SQL Server e Oracle. Trocar de banco em um projeto NestJS exige, na maioria dos casos, apenas alterar a configuração de conexão.
Migrations com controle total
Diferente de abordagens que sincronizam o schema automaticamente (perigoso em produção), o TypeORM oferece um sistema de migrations que permite versionar alterações no banco de forma segura e auditável.
Casos de uso reais onde essa combinação brilha
A dupla NestJS mais TypeORM é particularmente eficaz em APIs de backoffice com muitas relações entre entidades, sistemas de e-commerce com regras de negócio complexas, plataformas SaaS multi-tenant e microsserviços que precisam de acesso estruturado a bancos relacionais. Em todos esses cenários, a arquitetura modular do NestJS combinada com o mapeamento relacional do TypeORM reduz drasticamente o boilerplate.
Setup completo: instalação e configuração
Criando o projeto NestJS
Comece criando um projeto novo com a CLI do NestJS:
npm install -g @nestjs/cli
nest new meu-projeto-api
cd meu-projeto-apiInstalando TypeORM e dependências
Para este guia, usaremos PostgreSQL como banco de dados. Instale os pacotes necessários:
npm install @nestjs/typeorm typeorm pg
npm install -D @nestjs/config dotenvSe preferir MySQL, substitua pg por mysql2. Para SQLite (útil em desenvolvimento), use better-sqlite3.
Configuração do módulo principal
Crie um arquivo .env na raiz do projeto:
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=app_user
DB_PASSWORD=senha_segura_aqui
DB_NAME=meu_projetoAgora configure o AppModule para carregar as variáveis de ambiente e inicializar o TypeORM:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_NAME'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
logging: configService.get<string>('NODE_ENV') === 'development',
}),
}),
UsersModule,
],
})
export class AppModule {}Note que synchronize está definido como false. Isso é intencional e crucial para ambientes de produção. Você pode aprofundar o tema de configuração segura de ambientes em outro guia sobre variáveis de ambiente e secrets em Node.js.
Nota sobre abordagens modernas
Vale mencionar que o TypeORM não é a única opção para NestJS. O Prisma, com seus driverAdapters e sistema de migrations mais declarativo, vem ganhando espaço como alternativa moderna. O MikroORM também é uma opção sólida com suporte oficial ao NestJS. Porém, o TypeORM continua sendo a integração mais madura e documentada para o ecossistema NestJS, especialmente em projetos que já estão em produção. Se você está começando um projeto novo, vale avaliar as alternativas, mas o TypeORM segue sendo uma escolha válida e bem suportada.
Exemplo prático: CRUD completo de usuários
Criando a entidade
// src/users/entities/user.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 100 })
name: string;
@Index({ unique: true })
@Column({ length: 255 })
email: string;
@Column({ select: false })
password: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}Criando o módulo, service e controller
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}
async findAll(): Promise<User[]> {
return this.usersRepository.find({
where: { isActive: true },
order: { createdAt: 'DESC' },
});
}
async findOne(id: string): Promise<User> {
const user = await this.usersRepository.findOne({
where: { id },
});
if (!user) {
throw new NotFoundException(`Usuário com ID ${id} não encontrado`);
}
return user;
}
async update(id: string, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id);
Object.assign(user, updateUserDto);
return this.usersRepository.save(user);
}
async remove(id: string): Promise<void> {
const user = await this.findOne(id);
await this.usersRepository.remove(user);
}
}// src/users/users.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
ParseUUIDPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.findOne(id);
}
@Put(':id')
update(
@Param('id', ParseUUIDPipe) id: string,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.remove(id);
}
}Gerenciando migrations
Para gerar e executar migrations, adicione os seguintes scripts ao package.json:
# Gerar uma migration a partir das diferenças entre entidades e o banco
npx typeorm migration:generate src/migrations/CreateUsersTable -d src/data-source.ts
# Executar migrations pendentes
npx typeorm migration:run -d src/data-source.ts
# Reverter a última migration executada
npx typeorm migration:revert -d src/data-source.ts
# Dropar o banco inteiro (CUIDADO: apenas desenvolvimento)
npx typeorm schema:drop -d src/data-source.tsVocê precisa de um arquivo data-source.ts separado para a CLI do TypeORM:
// src/data-source.ts
import { DataSource } from 'typeorm';
import * as dotenv from 'dotenv';
dotenv.config();
export default new DataSource({
type: 'postgres',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
migrations: [__dirname + '/migrations/*{.ts,.js}'],
});Boas práticas para projetos reais
Nunca use synchronize em produção
A opção synchronize: true altera o schema do banco automaticamente toda vez que a aplicação inicia. Em produção, isso pode causar perda de dados. Use sempre migrations.
Separe configurações por ambiente
Use o ConfigModule do NestJS com validação via class-validator ou joi para garantir que todas as variáveis obrigatórias estejam definidas antes da aplicação iniciar.
Use o padrão Repository com cuidado
O TypeORM permite usar tanto o padrão Repository quanto o Active Record. No NestJS, o padrão Repository é preferível porque se integra naturalmente com a injeção de dependências. Evite acessar o DataSource diretamente nos controllers.
Implemente soft delete quando apropriado
Para entidades que não devem ser removidas permanentemente, use a coluna @DeleteDateColumn() do TypeORM combinada com o método softRemove(). Isso mantém o histórico e facilita auditorias.
Segurança no acesso ao banco de dados
Use credenciais com menor privilégio: nunca conecte sua aplicação com o usuário root ou admin do banco de dados. Crie um usuário específico para a aplicação com permissões apenas de SELECT, INSERT, UPDATE e DELETE nas tabelas necessárias. O usuário que executa migrations pode ter permissões adicionais de DDL, mas deve ser diferente do usuário da aplicação em runtime. Armazene credenciais em serviços de secrets (AWS Secrets Manager, HashiCorp Vault) e nunca as versione em repositórios.
Relações e eager loading
Evite eager: true em relações do TypeORM. Prefira carregar relações explicitamente com a opção relations no find ou usando QueryBuilder. O eager loading silencioso é uma das maiores causas de problemas de performance em projetos TypeORM. Você pode aprofundar técnicas de otimização de queries em outro guia sobre performance em APIs Node.js.
Erros Comuns e Soluções
EntityMetadataNotFoundError: No metadata for "User" was found
Esse erro indica que o TypeORM não encontrou a entidade registrada. Verifique se o caminho em entities no data-source.ts ou no TypeOrmModule.forRoot está correto. Solução concreta: confirme que o glob pattern [__dirname + '/**/*.entity{.ts,.js}'] está resolvendo para os arquivos certos, ou registre as entidades explicitamente passando as classes no array.
RepositoryNotFoundError: No repository for "User" was found
Esse erro acontece quando você tenta injetar um repositório em um módulo que não importou TypeOrmModule.forFeature([User]). Solução: adicione TypeOrmModule.forFeature([User]) no array imports do módulo que precisa acessar aquela entidade.
QueryFailedError: relation "users" does not exist
O banco não possui a tabela esperada. Isso geralmente ocorre quando as migrations não foram executadas ou quando synchronize está false e nenhuma migration foi gerada. Solução:
npx typeorm migration:run -d src/data-source.tsQueryFailedError: duplicate key value violates unique constraint
Você está tentando inserir um registro com valor duplicado em uma coluna com índice único. Solução: implemente verificação prévia no service antes de salvar, ou capture o erro com um filtro de exceções customizado que retorne HTTP 409 (Conflict).
const existingUser = await this.usersRepository.findOne({
where: { email: createUserDto.email },
});
if (existingUser) {
throw new ConflictException('E-mail já cadastrado');
}AlreadyHasActiveConnectionError: Cannot create a new connection named "default"
Esse erro aparece quando múltiplas instâncias do TypeORM tentam criar conexões com o mesmo nome. É comum em testes automatizados. Solução: antes de criar uma nova conexão em testes, destrua a anterior:
const dataSource = app.get(DataSource);
if (dataSource.isInitialized) {
await dataSource.destroy();
}Migration gerada vazia (sem alterações detectadas)
O comando migration:generate compara o estado atual do banco com as entidades. Se o banco já está sincronizado, a migration será vazia. Isso também acontece quando o data-source.ts não está apontando para as entidades corretas. Solução: verifique o path das entidades e confirme que o banco ao qual você está conectado é o correto para aquele ambiente.
Timeout na conexão com o banco
Erros como Connection terminated unexpectedly ou ETIMEDOUT indicam problemas de rede ou configuração. Solução: verifique se o host e porta estão corretos, se o banco aceita conexões externas (cheque pg_hba.conf no PostgreSQL) e considere adicionar configurações de pool:
extra: {
max: 10,
connectionTimeoutMillis: 5000,
}Ciclo completo: cleanup e destruição de recursos
Em ambientes de desenvolvimento e CI/CD, é importante saber como limpar tudo:
# Reverter todas as migrations (executar repetidamente)
npx typeorm migration:revert -d src/data-source.ts
# Dropar todo o schema do banco
npx typeorm schema:drop -d src/data-source.ts
# Se estiver usando Docker para o banco
docker stop postgres-local
docker rm postgres-local
docker volume rm pgdata
# Remover dependências do projeto
rm -rf node_modules
rm -rf distConclusão
NestJS com TypeORM é uma combinação testada em batalha que funciona bem para projetos de diversos tamanhos. A arquitetura modular do NestJS mantém o código organizado, enquanto o TypeORM cuida da comunicação com o banco de forma tipada e segura.
Os pontos essenciais para levar deste guia: mantenha synchronize desabilitado em produção, use migrations para todas as alterações de schema, aplique o princípio de menor privilégio nas credenciais do banco, injete repositórios via módulos e evite eager loading implícito.
Se você seguir essas práticas desde o início do projeto, vai economizar horas de debugging e refatoração. A curva de aprendizado inicial compensa rapidamente quando o projeto cresce e você precisa de previsibilidade e manutenibilidade no acesso a dados.
Guias Relacionados
Explore mais guias técnicos
Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.
Ver todos os guias