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: trueagainst a production database. Set it from an environment variable and default it tofalse. 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 useALTER TABLE ... RENAME COLUMNso 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
| Command | What it does | Touches the DB? |
|---|---|---|
migration:generate <path> | Diffs entities vs. schema and writes SQL | No (reads schema only) |
migration:create <path> | Scaffolds an empty migration | No |
migration:run | Applies all pending migrations | Yes |
migration:revert | Rolls back the last applied migration | Yes |
migration:show | Lists applied and pending migrations | Reads only |
Best practices
- Keep
synchronize: falseeverywhere 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:runautomatically 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:createfor data backfills and wrap multi-statement changes in the providedQueryRunnertransaction.