Versionamento de Schema com TypeORM: Migrations do Desenvolvimento ao Deploy
O erro que destrói tabelas em produção
Uma migration com synchronize: true em produção apaga colunas. Não é bug do TypeORM: é comportamento documentado. O synchronize compara as entities do código com o schema atual e executa ALTER TABLE para alinhar os dois. Se você removeu uma propriedade da entity (ou renomeou), o synchronize interpreta como "drop column". Sem confirmação, sem rollback, sem aviso.
Esse é o ponto de partida deste post: migrations existem para que mudanças de schema sejam intencionais, rastreáveis e reversíveis. O TypeORM oferece tooling para isso, mas a configuração padrão empurra você na direção errada.
Configuração base do DataSource
O TypeORM 0.3+ substituiu ormconfig.json pelo DataSource. A configuração abaixo funciona para PostgreSQL e separa o comportamento por ambiente:
// src/data-source.ts
import { DataSource } from "typeorm";
import { join } from "path";
// Migrations ficam em pasta separada das entities
// para que o CLI encontre os arquivos compilados em produção
const isProduction = process.env.NODE_ENV === "production";
export const AppDataSource = new DataSource({
type: "postgres",
host: process.env.DB_HOST ?? "localhost",
port: Number(process.env.DB_PORT ?? 5432),
username: process.env.DB_USER ?? "app",
password: process.env.DB_PASS ?? "secret",
database: process.env.DB_NAME ?? "myapp",
// synchronize NUNCA true em produção
synchronize: false,
// logging de queries só em dev para diagnóstico
logging: isProduction ? ["error", "migration"] : true,
entities: [join(__dirname, "entities", "*.{ts,js}")],
migrations: [join(__dirname, "migrations", "*.{ts,js}")],
});
O synchronize: false é a única configuração segura para qualquer ambiente que persista dados reais. Em desenvolvimento, o fluxo correto é gerar migrations a partir das mudanças nas entities.
Gerando migrations a partir das entities
O CLI do TypeORM compara as entities registradas no DataSource com o schema atual do banco e gera o SQL necessário. Para que isso funcione, o banco de desenvolvimento precisa estar rodando e acessível.
Primeiro, configure o script no package.json:
{
"scripts": {
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"migration:generate": "npm run typeorm -- migration:generate -d src/data-source.ts",
"migration:run": "npm run typeorm -- migration:run -d src/data-source.ts",
"migration:revert": "npm run typeorm -- migration:revert -d src/data-source.ts",
"migration:create": "npm run typeorm -- migration:create"
}
}
Com uma entity definida:
// src/entities/Product.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm";
@Entity("products")
export class Product {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ type: "varchar", length: 255 })
name: string;
@Column({ type: "integer" })
priceInCents: number;
// Coluna nullable porque produtos existentes não terão SKU preenchido
@Column({ type: "varchar", length: 50, nullable: true })
sku: string | null;
@CreateDateColumn({ type: "timestamptz" })
createdAt: Date;
}
Gere a migration:
npm run migration:generate -- src/migrations/CreateProducts
O TypeORM cria um arquivo com timestamp e o SQL inferido. O resultado se parece com isto:
// src/migrations/1719432000000-CreateProducts.ts
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateProducts1719432000000 implements MigrationInterface {
name = "CreateProducts1719432000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "products" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"name" character varying(255) NOT NULL,
"priceInCents" integer NOT NULL,
"sku" character varying(50),
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_products_
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/backend/typeorm-migrations-versionamento-schema-desenvolvimento-deploy](https://www.vivodecodigo.com.br/backend/typeorm-migrations-versionamento-schema-desenvolvimento-deploy)