TypeORM Migrations

DataSource Configuration

// data-source.ts
import "reflect-metadata";
import { DataSource } from "typeorm";

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

// Initialize in app startup
await AppDataSource.initialize();

CLI Commands

Add a script to package.json: "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --dataSource src/data-source.ts"

# Generate migration from entity changes
npm run typeorm migration:generate src/migrations/AddUserRole

# Create blank migration
npm run typeorm migration:create src/migrations/SeedInitialData

# Run pending migrations
npm run typeorm migration:run

# Revert last migration
npm run typeorm migration:revert

# Show migration status
npm run typeorm migration:show

# Sync schema WITHOUT migrations (dev only!)
npm run typeorm schema:sync

# Drop all tables (dangerous!)
npm run typeorm schema:drop

# Log SQL that would be generated
npm run typeorm schema:log

Custom Migration Scripts

// src/migrations/1705312000000-AddUserRole.ts
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";

export class AddUserRole1705312000000 implements MigrationInterface {
  name = "AddUserRole1705312000000";

  async up(queryRunner: QueryRunner): Promise {
    // Add column
    await queryRunner.addColumn("users", new TableColumn({
      name:    "role",
      type:    "enum",
      enum:    ["admin", "user", "guest"],
      default: "'user'"
    }));

    // Add index
    await queryRunner.createIndex("users", {
      name:    "idx_users_role",
      columnNames: ["role"]
    } as any);

    // Raw SQL (data migration)
    await queryRunner.query(`
      UPDATE users SET role = 'admin'
      WHERE email LIKE '%@company.com'
    `);
  }

  async down(queryRunner: QueryRunner): Promise {
    await queryRunner.dropIndex("users", "idx_users_role");
    await queryRunner.dropColumn("users", "role");
  }
}

QueryRunner Methods

// Table operations
await queryRunner.createTable(new Table({ name: "...", columns: [...] }));
await queryRunner.dropTable("table_name");
await queryRunner.renameTable("old", "new");

// Column operations
await queryRunner.addColumn("users", new TableColumn({ ... }));
await queryRunner.dropColumn("users", "column_name");
await queryRunner.changeColumn("users", "old_col", new TableColumn({ ... }));
await queryRunner.renameColumn("users", "old", "new");

// Index & FK
await queryRunner.createIndex("users", new TableIndex({ ... }));
await queryRunner.dropIndex("users", "idx_name");
await queryRunner.createForeignKey("orders", new TableForeignKey({ ... }));
await queryRunner.dropForeignKey("orders", "fk_name");

// Raw SQL
await queryRunner.query("CREATE EXTENSION IF NOT EXISTS pgcrypto");

// Transaction control (migrations run in transactions by default)
await queryRunner.startTransaction();
await queryRunner.commitTransaction();
await queryRunner.rollbackTransaction();

Programmatic Migration Run

// Run migrations programmatically (e.g., on app startup)
import { AppDataSource } from "./data-source";

async function runMigrations() {
  await AppDataSource.initialize();

  const pending = await AppDataSource.showMigrations();
  if (pending) {
    console.log("Running pending migrations...");
    await AppDataSource.runMigrations({ transaction: "all" });
    console.log("Migrations complete");
  }
}

// transaction option:
// "all"    - wrap all migrations in one transaction
// "none"   - no transaction
// "each"   - separate transaction per migration