Lesson 27 of 5022 min read

NestJS Database Migrations Tutorial – Step-by-Step Guide

Learn what database migrations are, why they matter for production applications, and how to generate, run, and revert migrations using TypeORM in NestJS.

Author: CodersNexus

NestJS Database Migrations Tutorial – Step-by-Step Guide

Earlier in this module, you learned that TypeORM's synchronize option should never be enabled in production because it can silently and dangerously alter your live database schema. Migrations are the proper, production-safe alternative: version-controlled, reviewable files that describe exactly how your database schema should change, applied deliberately and in a specific, trackable order.

This lesson explains what migrations are, why every serious production application relies on them, and walks through the complete workflow of generating, running, and reverting migrations using TypeORM's CLI within a NestJS project.

NestJS Database Migrations: Learning Objectives

  • Understand what a database migration is and why it is essential for production applications.
  • Configure TypeORM's CLI to generate and run migrations in a NestJS project.
  • Generate a migration automatically based on entity changes.
  • Run pending migrations against a database.
  • Revert the most recently applied migration when needed.

NestJS Database Migrations: Key Terms and Definitions

  • Migration: A version-controlled file containing instructions to apply (and typically also reverse) a specific change to a database schema.
  • Migration history table: A special table TypeORM creates and maintains to track which migrations have already been applied to a given database.
  • up() method: The method within a migration file containing the logic to apply the schema change, such as creating a table or adding a column.
  • down() method: The method within a migration file containing the logic to reverse the schema change made by the corresponding up() method.
  • DataSource: TypeORM's configuration object (in modern versions) used both by the application at runtime and by the TypeORM CLI to connect to the database for migration purposes.

How NestJS Database Migrations Works: Detailed Explanation

A migration is fundamentally a small, version-controlled program describing one specific change to your database schema, such as creating a new table, adding a column, or modifying a constraint. Every migration file contains two key methods: up(), which contains the logic to apply the change, and down(), which contains the logic to precisely reverse it. This up/down pairing is what allows a team to move a database schema both forward (applying new changes) and backward (undoing a problematic change) in a controlled, predictable way.

Unlike the synchronize option, which automatically and immediately alters your schema to match your current entities, migrations require a deliberate, two-step process: first generating a migration file (or writing one by hand), and then explicitly running it against a target database. This separation is exactly what makes migrations safe for production: a generated migration file can be reviewed by a teammate, tested against a staging database, and version-controlled alongside your application code, all before it ever touches production data.

To use migrations, TypeORM's CLI needs its own way to connect to your database, typically configured through a dedicated DataSource configuration file, separate from (though often sharing values with) your application's runtime TypeOrmModule configuration. With this DataSource properly configured, running the CLI's migration:generate command compares your current entity definitions against the actual current state of the target database and automatically generates a migration file containing the SQL statements needed to bring the database in line with your entities, which you should always review before running.

Once a migration file exists, running migration:run executes any pending migrations, in order, against the configured database, and TypeORM tracks which migrations have already been applied using a dedicated migrations history table, ensuring the same migration is never accidentally run twice. If a migration turns out to be problematic after being applied, migration:revert undoes the single most recently applied migration by executing its down() method, providing a controlled way to roll back a specific schema change if needed.

This entire workflow, generate, review, run, and if necessary revert, replaces the convenience but danger of synchronize with a slower, more deliberate process that is dramatically safer for any application handling real, valuable production data.

Interview-Friendly Explanation

A strong interview or viva answer for nestjs database migrations must go beyond a one-line definition. Interviewers evaluating BCA, MCA, B.Tech, and self-taught backend developers reward answers that combine a precise definition, a clear working explanation, a real-world example, and a conclusion that highlights why it matters in production Node.js applications. Use the four-point framework below when answering under time pressure.

  • Definition point: Migration: A version-controlled file containing instructions to apply (and typically also reverse) a specific change to a database schema.
  • Working point: Configure TypeORM's CLI to generate and run migrations in a NestJS project.
  • Example point: Every serious production application backed by a relational database relies on migrations, generated and reviewed as part of the normal pull request process, before any schema change reaches production.
  • Conclusion point: Migrations trade the convenience of synchronize for the safety and control every production application actually needs.

How to Answer This in a Technical Interview

  1. Give a two-to-three sentence definition using correct database and ORM terminology.
  2. Add one specific example drawn from a real backend scenario such as an e-commerce, fintech, or SaaS database schema.
  3. Mention any trade-offs versus alternative approaches (e.g. TypeORM vs Prisma, SQL vs NoSQL) since interviewers often probe this contrast.
  4. Close with one benefit, limitation, or production use case.
  5. Avoid vague answers like "it just connects to the database" — interviewers filter these out immediately.

Practical Scenario

Imagine you are designing the data layer for a production SaaS product, similar to systems used at companies like Razorpay, Freshworks, or Zomato. Every lesson in this module maps directly to a decision you will make while building that backend: how data is structured, how it's queried efficiently, how relationships between entities are modeled, and how the schema evolves safely over time as the product grows.

NestJS Database Migrations: Architecture and Flow Diagram

Visualize the migration lifecycle:

[Entity changes made in code] --> [typeorm migration:generate] --> [Migration file created with up() and down() methods] --> [Review the generated SQL] --> [typeorm migration:run applies it to the database] --> [Migrations history table records it as applied]

If needed: [typeorm migration:revert] --> [Executes down() to undo the most recent migration]

NestJS Database Migrations: Command Reference Table

CommandPurpose
migration:generateCompares entities against the current database schema and generates a migration file
migration:createCreates an empty migration file for manually writing custom schema changes
migration:runExecutes all pending (not yet applied) migrations against the target database
migration:revertReverses the most recently applied migration by executing its down() method
migration:showLists which migrations have been applied and which are still pending

NestJS Database Migrations: NestJS Code Example

# Step 1: Ensure a DataSource configuration file exists (data-source.ts)
# This is typically separate from, but similar to, your app's TypeOrmModule config

# Step 2: Generate a migration based on current entity changes
# npx typeorm-ts-node-commonjs migration:generate ./src/migrations/AddIsActiveToUsers -d ./src/data-source.ts

# Example of a generated migration file (simplified):
# src/migrations/1719999999999-AddIsActiveToUsers.ts
#
# import { MigrationInterface, QueryRunner } from 'typeorm';
#
# export class AddIsActiveToUsers1719999999999 implements MigrationInterface {
#   public async up(queryRunner: QueryRunner): Promise<void> {
#     await queryRunner.query(
#       `ALTER TABLE \`users\` ADD \`isActive\` tinyint NOT NULL DEFAULT 1`,
#     );
#   }
#
#   public async down(queryRunner: QueryRunner): Promise<void> {
#     await queryRunner.query(`ALTER TABLE \`users\` DROP COLUMN \`isActive\``);
#   }
# }

# Step 3: Run pending migrations against the database
# npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts

# Step 4: If something goes wrong, revert the most recent migration
# npx typeorm-ts-node-commonjs migration:revert -d ./src/data-source.ts

# Step 5: Check migration status at any time
# npx typeorm-ts-node-commonjs migration:show -d ./src/data-source.ts

This workflow shows the complete migration lifecycle: after adding an isActive property to a User entity, running migration:generate compares the entity against the current database and produces a migration file like AddIsActiveToUsers, whose up() method adds the new column and whose down() method removes it again, giving you a precise, reversible record of this exact schema change. Running migration:run then applies this (and any other pending migrations) to the target database, while migration:revert provides a safety net, allowing you to cleanly undo the most recent migration if a problem is discovered after deployment. migration:show gives visibility into exactly which migrations have already been applied to a given database at any point.

Real-World NestJS Database Migrations Industry Examples

  • Every serious production application backed by a relational database relies on migrations, generated and reviewed as part of the normal pull request process, before any schema change reaches production.
  • Companies with strict compliance requirements, such as fintech and healthcare, often require migrations to be reviewed and approved by a database administrator or senior engineer before being run against production, precisely because of the risks synchronize would otherwise introduce.
  • CI/CD pipelines commonly include an automated step that runs pending migrations against a staging database as part of the deployment process, verifying schema changes apply cleanly before touching production.
  • Teams practicing careful database change management always commit generated migration files to version control alongside the corresponding entity changes, keeping the two permanently in sync and reviewable together.

NestJS Database Migrations Interview Questions and Answers

Q1. What is a database migration and why is it preferred over TypeORM's synchronize option in production?

Short answer: A migration is a version-controlled file containing explicit instructions (via up() and down() methods) to apply and reverse a specific database schema change. It is preferred in production because, unlike synchronize, it requires a deliberate generate-then-run process that can be reviewed, tested, and tracked, avoiding the risk of automatic, unreviewed schema changes silently affecting live data.

Detailed explanation: A migration is fundamentally a small, version-controlled program describing one specific change to your database schema, such as creating a new table, adding a column, or modifying a constraint. Every migration file contains two key methods: up(), which contains the logic to apply the change, and down(), which contains the logic to precisely reverse it. This up/down pairing is what allows a team to move a database schema both forward (applying new changes) and backward (undoing a problematic change) in a controlled, predictable way. Unlike the synchronize option, which automatically and immediately alters your schema to match your current entities, migrations require a deliberate, two-step process: first generating a migration file (or writing one by hand), and then explicitly running it against a target database. This separation is exactly what makes migrations safe for production: a generated migration file can be reviewed by a teammate, tested against a staging database, and version-controlled alongside your application code, all before it ever touches production data. To use migrations, TypeORM's CLI needs its own way to connect to your database, typically configured through a dedicated DataSource configuration file, separate from (though often sharing values with) your application's runtime TypeOrmModule configuration. With this DataSource properly configured, running the CLI's migration:generate command compares your current entity definitions against the actual current state of the target database and automatically generates a migration file containing the SQL statements needed to bring the database in line with your entities, which you should always review before running. Once a migration file exists, running migration:run executes any pending migrations, in order, against the configured database, and TypeORM tracks which migrations have already been applied using a dedicated migrations history table, ensuring the same migration is never accidentally run twice. If a migration turns out to be problematic after being applied, migration:revert undoes the single most recently applied migration by executing its down() method, providing a controlled way to roll back a specific schema change if needed. This entire workflow, generate, review, run, and if necessary revert, replaces the convenience but danger of synchronize with a slower, more deliberate process that is dramatically safer for any application handling real, valuable production data.

Practical example: Every serious production application backed by a relational database relies on migrations, generated and reviewed as part of the normal pull request process, before any schema change reaches production.

Interview tip: Migrations replace synchronize for production-safe, reviewable, version-controlled schema changes.

Revision hook: Migrations trade the convenience of synchronize for the safety and control every production application actually needs.

Q2. What is the purpose of the up() and down() methods in a migration file?

Short answer: The up() method contains the logic to apply a specific schema change, such as adding a column or creating a table. The down() method contains the logic to precisely reverse that same change, allowing the migration to be safely rolled back using migration:revert if a problem is discovered after it has been applied.

Detailed explanation: This workflow shows the complete migration lifecycle: after adding an isActive property to a User entity, running migration:generate compares the entity against the current database and produces a migration file like AddIsActiveToUsers, whose up() method adds the new column and whose down() method removes it again, giving you a precise, reversible record of this exact schema change. Running migration:run then applies this (and any other pending migrations) to the target database, while migration:revert provides a safety net, allowing you to cleanly undo the most recent migration if a problem is discovered after deployment. migration:show gives visibility into exactly which migrations have already been applied to a given database at any point.

Practical example: Companies with strict compliance requirements, such as fintech and healthcare, often require migrations to be reviewed and approved by a database administrator or senior engineer before being run against production, precisely because of the risks synchronize would otherwise introduce.

Interview tip: Every migration file implements up() (apply change) and down() (reverse change) methods.

Revision hook: Generated migrations should always be reviewed, not blindly trusted, before being run against any real database.

Q3. How does TypeORM know which migrations have already been applied to a given database?

Short answer: TypeORM automatically creates and maintains a dedicated migrations history table within the target database, recording which migration files have already been run, which prevents the same migration from being accidentally executed more than once and allows migration:show to report accurate status.

Detailed explanation: Database migrations provide a deliberate, version-controlled, and reversible alternative to TypeORM's dangerous synchronize option, essential for any application managing real production data. Each migration file implements an up() method describing how to apply a specific schema change and a down() method describing how to precisely reverse it. Using TypeORM's CLI, developers generate migrations based on differences between their entities and the current database schema with migration:generate, apply pending migrations using migration:run, and can safely undo the most recently applied migration using migration:revert if a problem arises. TypeORM tracks exactly which migrations have been applied to a given database using a dedicated history table, ensuring migrations are never accidentally run twice and giving teams full visibility and control over how their database schema evolves over time.

Practical example: CI/CD pipelines commonly include an automated step that runs pending migrations against a staging database as part of the deployment process, verifying schema changes apply cleanly before touching production.

Interview tip: Key CLI commands: migration:generate, migration:run, migration:revert, migration:show.

Revision hook: The up()/down() pairing gives you a genuine, tested path to roll back a problematic schema change.

Q4. Walk through the typical workflow for adding a new column to an existing table using migrations.

Short answer: You would first update the corresponding entity class to include the new property, then run migration:generate, which compares the updated entity against the current database schema and automatically produces a migration file with the appropriate ALTER TABLE statement in its up() method. After reviewing this generated file, you would run migration:run to apply it to the target database.

Detailed explanation: A migration is fundamentally a small, version-controlled program describing one specific change to your database schema, such as creating a new table, adding a column, or modifying a constraint. Every migration file contains two key methods: up(), which contains the logic to apply the change, and down(), which contains the logic to precisely reverse it. This up/down pairing is what allows a team to move a database schema both forward (applying new changes) and backward (undoing a problematic change) in a controlled, predictable way. Unlike the synchronize option, which automatically and immediately alters your schema to match your current entities, migrations require a deliberate, two-step process: first generating a migration file (or writing one by hand), and then explicitly running it against a target database. This separation is exactly what makes migrations safe for production: a generated migration file can be reviewed by a teammate, tested against a staging database, and version-controlled alongside your application code, all before it ever touches production data. To use migrations, TypeORM's CLI needs its own way to connect to your database, typically configured through a dedicated DataSource configuration file, separate from (though often sharing values with) your application's runtime TypeOrmModule configuration. With this DataSource properly configured, running the CLI's migration:generate command compares your current entity definitions against the actual current state of the target database and automatically generates a migration file containing the SQL statements needed to bring the database in line with your entities, which you should always review before running. Once a migration file exists, running migration:run executes any pending migrations, in order, against the configured database, and TypeORM tracks which migrations have already been applied using a dedicated migrations history table, ensuring the same migration is never accidentally run twice. If a migration turns out to be problematic after being applied, migration:revert undoes the single most recently applied migration by executing its down() method, providing a controlled way to roll back a specific schema change if needed. This entire workflow, generate, review, run, and if necessary revert, replaces the convenience but danger of synchronize with a slower, more deliberate process that is dramatically safer for any application handling real, valuable production data.

Practical example: Teams practicing careful database change management always commit generated migration files to version control alongside the corresponding entity changes, keeping the two permanently in sync and reviewable together.

Interview tip: TypeORM tracks applied migrations using a dedicated history table within the target database.

Revision hook: Treating migration files as first-class, version-controlled code (reviewed in pull requests) is standard professional practice.

NestJS Database Migrations MCQs and Practice Questions

1. What is the main advantage of migrations over TypeORM's synchronize option?

  1. They make queries run faster
  2. They provide a deliberate, reviewable, and reversible way to change a database schema
  3. They eliminate the need for entities
  4. They automatically back up the database

Answer: B. They provide a deliberate, reviewable, and reversible way to change a database schema

Explanation: Unlike synchronize's automatic, unreviewed schema changes, migrations require a deliberate generate-then-run process that can be reviewed, tested, and reversed, making them far safer for production use.

Concept link: Migrations replace synchronize for production-safe, reviewable, version-controlled schema changes.

Why this matters: Migrations trade the convenience of synchronize for the safety and control every production application actually needs.

2. Which method in a migration file contains the logic to reverse a schema change?

  1. up()
  2. down()
  3. revert()
  4. rollback()

Answer: B. down()

Explanation: The down() method contains the logic to precisely undo whatever schema change the corresponding up() method applied, enabling controlled rollback via migration:revert.

Concept link: Every migration file implements up() (apply change) and down() (reverse change) methods.

Why this matters: Generated migrations should always be reviewed, not blindly trusted, before being run against any real database.

3. Which command generates a new migration file based on differences between your entities and the current database schema?

  1. migration:run
  2. migration:revert
  3. migration:generate
  4. migration:show

Answer: C. migration:generate

Explanation: migration:generate compares your current entity definitions against the actual state of the target database and automatically produces a migration file containing the necessary schema changes.

Concept link: Key CLI commands: migration:generate, migration:run, migration:revert, migration:show.

Why this matters: The up()/down() pairing gives you a genuine, tested path to roll back a problematic schema change.

4. What does TypeORM use to track which migrations have already been applied to a database?

  1. A JSON file on disk
  2. A dedicated migrations history table in the database
  3. Git commit history
  4. The application's log files

Answer: B. A dedicated migrations history table in the database

Explanation: TypeORM automatically creates and maintains a special table within the target database itself to record which migrations have already run, ensuring migrations are never accidentally re-applied.

Concept link: TypeORM tracks applied migrations using a dedicated history table within the target database.

Why this matters: Treating migration files as first-class, version-controlled code (reviewed in pull requests) is standard professional practice.

Common NestJS Database Migrations Mistakes to Avoid

  • Blindly running migration:run without first reviewing the generated migration file's SQL, potentially applying an unintended or incorrect schema change.
  • Forgetting to commit generated migration files to version control, leaving other developers and environments without the schema changes needed to stay in sync.
  • Writing a down() method that doesn't correctly and fully reverse the corresponding up() method, breaking the ability to safely roll back that migration later.
  • Using synchronize and migrations together inconsistently across environments, leading to confusing schema drift between development and production databases.

NestJS Database Migrations: Interview Notes and Exam Tips

  • Migrations replace synchronize for production-safe, reviewable, version-controlled schema changes.
  • Every migration file implements up() (apply change) and down() (reverse change) methods.
  • Key CLI commands: migration:generate, migration:run, migration:revert, migration:show.
  • TypeORM tracks applied migrations using a dedicated history table within the target database.

Key NestJS Database Migrations Takeaways

  • Migrations trade the convenience of synchronize for the safety and control every production application actually needs.
  • Generated migrations should always be reviewed, not blindly trusted, before being run against any real database.
  • The up()/down() pairing gives you a genuine, tested path to roll back a problematic schema change.
  • Treating migration files as first-class, version-controlled code (reviewed in pull requests) is standard professional practice.

NestJS Database Migrations: Summary

Database migrations provide a deliberate, version-controlled, and reversible alternative to TypeORM's dangerous synchronize option, essential for any application managing real production data. Each migration file implements an up() method describing how to apply a specific schema change and a down() method describing how to precisely reverse it. Using TypeORM's CLI, developers generate migrations based on differences between their entities and the current database schema with migration:generate, apply pending migrations using migration:run, and can safely undo the most recently applied migration using migration:revert if a problem arises. TypeORM tracks exactly which migrations have been applied to a given database using a dedicated history table, ensuring migrations are never accidentally run twice and giving teams full visibility and control over how their database schema evolves over time.

Frequently Asked Questions

While technically possible, it is not recommended, since having both active can lead to confusing schema drift, where synchronize makes automatic changes that migrations aren't aware of, or vice versa. Best practice is to use synchronize only in early local development and switch entirely to migrations before any shared or production environment. In interviews, tie this back to: Migrations replace synchronize for production-safe, reviewable, version-controlled schema changes. In real applications, consider this example: Every serious production application backed by a relational database relies on migrations, generated and reviewed as part of the normal pull request process, before any schema change reaches production. Key revision takeaway: Migrations trade the convenience of synchronize for the safety and control every production application actually needs.

The database's actual schema will drift out of sync with what your migration history and entity definitions describe, which can cause migration:generate to produce confusing or incorrect results later, and makes it harder for other developers or environments to reliably reproduce the exact same schema. In interviews, tie this back to: Every migration file implements up() (apply change) and down() (reverse change) methods. In real applications, consider this example: Companies with strict compliance requirements, such as fintech and healthcare, often require migrations to be reviewed and approved by a database administrator or senior engineer before being run against production, precisely because of the risks synchronize would otherwise introduce. Key revision takeaway: Generated migrations should always be reviewed, not blindly trusted, before being run against any real database.

Generally, no. Once a migration has been applied and recorded in the migrations history table, editing its content can cause inconsistencies, since the applied change on the database no longer matches what the migration file describes. If a mistake needs fixing, it's safer to create a new migration correcting the issue. In interviews, tie this back to: Key CLI commands: migration:generate, migration:run, migration:revert, migration:show. In real applications, consider this example: CI/CD pipelines commonly include an automated step that runs pending migrations against a staging database as part of the deployment process, verifying schema changes apply cleanly before touching production. Key revision takeaway: The up()/down() pairing gives you a genuine, tested path to roll back a problematic schema change.

By default, migration:revert undoes exactly one migration: the most recently applied one, based on TypeORM's migrations history table. To undo multiple migrations, you would need to run migration:revert multiple times in succession. In interviews, tie this back to: TypeORM tracks applied migrations using a dedicated history table within the target database. In real applications, consider this example: Teams practicing careful database change management always commit generated migration files to version control alongside the corresponding entity changes, keeping the two permanently in sync and reviewable together. Key revision takeaway: Treating migration files as first-class, version-controlled code (reviewed in pull requests) is standard professional practice.

In modern TypeORM versions, yes, a dedicated DataSource file is typically required for the CLI to connect to your database for migration purposes, though it commonly shares the same connection details (host, port, credentials) as your application's runtime TypeOrmModule configuration. In interviews, tie this back to: Migrations replace synchronize for production-safe, reviewable, version-controlled schema changes. In real applications, consider this example: Every serious production application backed by a relational database relies on migrations, generated and reviewed as part of the normal pull request process, before any schema change reaches production. Key revision takeaway: Migrations trade the convenience of synchronize for the safety and control every production application actually needs.

This usually means TypeORM detected no differences between your current entity definitions and the actual state of the target database, meaning either your entities haven't changed since the last migration, or the CLI's DataSource configuration isn't pointing to the database and entities you expect. In interviews, tie this back to: Every migration file implements up() (apply change) and down() (reverse change) methods. In real applications, consider this example: Companies with strict compliance requirements, such as fintech and healthcare, often require migrations to be reviewed and approved by a database administrator or senior engineer before being run against production, precisely because of the risks synchronize would otherwise introduce. Key revision takeaway: Generated migrations should always be reviewed, not blindly trusted, before being run against any real database.