Skip to content
NestJS ns database 4 min read

TypeORM Migrations

Migrations are versioned, reviewable SQL scripts that evolve your database schema over time. Instead of letting TypeORM mutate tables automatically, you commit each change to source control and apply it deterministically across every environment. This page shows how to wire up a CLI data source, generate migrations from entity diffs, run and revert them, and why synchronize: true is dangerous in production.

Why migrations instead of synchronize

TypeORM’s synchronize option compares your entities to the live schema on every boot and silently alters tables to match. It’s convenient for prototyping but reckless in production: a renamed column looks like a drop + add to TypeORM, so it will happily delete the old column and all its data with no confirmation and no audit trail.

Migrations invert this control. Each schema change becomes an explicit, timestamped class with up() and down() methods that you review, test, and replay in order.

Warning: Never enable synchronize: true against a production database. Set it from an environment variable and default it to false. One accidental deploy can drop columns or whole tables.

Configuring a data source for the CLI

The TypeORM CLI runs outside of Nest’s dependency injection, so it needs its own DataSource instance pointing at the same database. Keep it in a dedicated file and reuse the connection options your AppModule already loads.

// src/database/data-source.ts
import 'reflect-metadata';
import { config } from 'dotenv';
import { DataSource } from 'typeorm';

config();

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 ?? 'postgres',
  password: process.env.DB_PASSWORD ?? 'postgres',
  database: process.env.DB_NAME ?? 'app',
  synchronize: false,
  logging: false,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/database/migrations/*.ts'],
});

The same options can be imported by your TypeOrmModule.forRoot() so the app and the CLI never drift apart.

CLI scripts

The CLI ships under typeorm. Because migrations are written in TypeScript, run them through ts-node. Add convenience scripts to package.json:

{
  "scripts": {
    "typeorm": "typeorm-ts-node-commonjs -d src/database/data-source.ts",
    "migration:generate": "npm run typeorm -- migration:generate",
    "migration:create": "npm run typeorm -- migration:create",
    "migration:run": "npm run typeorm -- migration:run",
    "migration:revert": "npm run typeorm -- migration:revert"
  }
}

The -d flag tells the CLI which data source to load.

Generating a migration

migration:generate diffs your entities against the current schema and writes the SQL needed to close the gap. Pass a path that includes the migration name; TypeORM prefixes it with a timestamp so files stay ordered.

npm run migration:generate -- src/database/migrations/AddUserTable

Output:

Migration /src/database/migrations/1718323200000-AddUserTable.ts has been generated successfully.

The generated class is fully populated — no editing required for simple changes:

// src/database/migrations/1718323200000-AddUserTable.ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddUserTable1718323200000 implements MigrationInterface {
  name = 'AddUserTable1718323200000';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE "user" (
        "id" SERIAL NOT NULL,
        "email" character varying NOT NULL,
        "createdAt" TIMESTAMP NOT NULL DEFAULT now(),
        CONSTRAINT "UQ_user_email" UNIQUE ("email"),
        CONSTRAINT "PK_user_id" PRIMARY KEY ("id")
      )
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE "user"`);
  }
}

Use migration:create instead of generate when you need a hand-written migration (data backfills, raw SQL) — it scaffolds an empty up/down pair.

Tip: Always read the generated SQL before committing. Renames in particular are emitted as drop-and-recreate; rewrite the up() to use ALTER TABLE ... RENAME COLUMN so you preserve data.

Running migrations

migration:run executes every pending migration in timestamp order inside a transaction, then records each one in the migrations metadata table so it never runs twice.

npm run migration:run

Output:

query: SELECT * FROM "migrations" "migrations" ORDER BY "id" DESC
0 migrations are already loaded in the database.
1 migrations were found in the source code.
AddUserTable1718323200000 is new migration must be executed.
query: START TRANSACTION
query: CREATE TABLE "user" (...)
Migration AddUserTable1718323200000 has been executed successfully.
query: COMMIT

Reverting migrations

migration:revert rolls back the most recent applied migration by calling its down() method, then removes its row from the metadata table. Run it repeatedly to step back through history one migration at a time.

npm run migration:revert

Output:

query: SELECT * FROM "migrations" "migrations" ORDER BY "id" DESC
query: START TRANSACTION
query: DROP TABLE "user"
Migration AddUserTable1718323200000 has been reverted successfully.
query: COMMIT

CLI command reference

CommandWhat it doesTouches the DB?
migration:generate <path>Diffs entities vs. schema and writes SQLNo (reads schema only)
migration:create <path>Scaffolds an empty migrationNo
migration:runApplies all pending migrationsYes
migration:revertRolls back the last applied migrationYes
migration:showLists applied and pending migrationsReads only

Best practices

  • Keep synchronize: false everywhere except throwaway local prototypes; let migrations be the single source of schema truth.
  • Commit every migration file to version control and never edit one that has already run in a shared environment — add a new migration instead.
  • Review generated SQL by hand; fix destructive renames before they reach migration:run.
  • Run migration:run automatically as a deploy step (CI/CD), not from application boot, so failures block the release.
  • Write meaningful down() methods so rollbacks are real, and test both directions locally.
  • Use migration:create for data backfills and wrap multi-statement changes in the provided QueryRunner transaction.
Last updated June 14, 2026
Was this helpful?