Database Migrations
Database Migrations are version control for your database schema. They are scripts that track and apply changes to your tables over time in a safe, reproducible manner.
Overview
During development, it is tempting to use synchronize: true in TypeORM to automatically update your tables when you change a TypeScript Entity class.
However, in production, if you change a column type from string to boolean, synchronize might just drop the column and recreate it, destroying all your users’ data!
Migrations solve this. A migration is a specific file containing raw SQL (or ORM commands) that says exactly how to upgrade the database from version 1 to version 2 (the up method), and how to downgrade it back to version 1 if something goes wrong (the down method).
Key Concepts
- Generation: Most ORMs have CLI tools that can look at your TypeScript entities, compare them to the current database schema, and automatically generate the migration script for you.
- Execution: Running the migration scripts against the production database (usually done in a CI/CD pipeline right before the new code is deployed).
- The Migrations Table: The ORM automatically creates a special table (e.g.,
migrations) in your database to track which migration files have already been run.
Code Examples
1. Generating a Migration (TypeORM CLI)
First, you modify your TypeScript entity (e.g., adding an age column to User). Then, you run the CLI command:
# Example TypeORM CLI command
npx typeorm migration:generate src/migrations/AddUserAge -d src/data-source.ts
TypeORM generates a file that looks like this:
// src/migrations/1689345920394-AddUserAge.ts
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddUserAge1689345920394 implements MigrationInterface {
name = 'AddUserAge1689345920394'
// The 'up' method is what happens when you deploy
public async up(queryRunner: QueryRunner): Promise<void> {
// The CLI automatically figured out the SQL needed!
await queryRunner.query(`ALTER TABLE "users" ADD "age" integer`);
}
// The 'down' method is what happens if you need to rollback
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "age"`);
}
}
2. Running Migrations
In your AppModule, you should NEVER have synchronize: true in production. Instead, you tell TypeORM where to find the migration files, and you enable migrationsRun: true.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
url: process.env.DATABASE_URL,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
// 1. NEVER use synchronize in production
synchronize: false,
// 2. Point to the compiled migration files
migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
// 3. Automatically run any pending migrations when the app starts
migrationsRun: process.env.NODE_ENV === 'production',
}),
],
})
export class AppModule {}
Best Practices
- CI/CD Execution: While setting
migrationsRun: truein NestJS is easy, it can be dangerous if you scale to 5 instances (they might all try to run the migration at the exact same time). The safest best practice is to run the migration CLI command (npx typeorm migration:run) as a separate step in your GitHub Actions / GitLab CI pipeline before the new servers start. - Never Modify Old Migrations: Once a migration file has been committed to
mainand run in production, never edit it. If you made a mistake, you must create a new migration file that fixes the mistake. Editing old files will break the migration history tracking.