NestJS — Enterprise Node.js

Migrations & Seeding

16 min Lesson 22 of 30

Migrations & Seeding

In development, synchronize: true auto-updates your schema — convenient but dangerous. In production you need migrations: versioned, reviewable scripts that change the database schema safely and reversibly. Paired with seeding (populating reference or test data), they give you full control over your database state.

Why migrations?

  • Safe — explicit changes, no surprise column drops.
  • Versioned — every schema change is committed to source control.
  • Reversible — each migration has an up (apply) and a down (roll back).
  • Repeatable — the same sequence runs identically on every environment.

Anatomy of a migration

A migration is a class with two methods. up() moves the schema forward; down() undoes it:

import { MigrationInterface, QueryRunner, Table } from 'typeorm'; export class CreateUsers1700000000000 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable(new Table({ name: 'users', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true }, { name: 'email', type: 'varchar', isUnique: true }, ], })); } async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('users'); } }

Generating and running migrations

TypeORM can generate a migration by diffing your entities against the current database — then you review it before applying:

# generate a migration from entity changes npx typeorm migration:generate ./src/migrations/AddUsers -d ./data-source.ts # apply pending migrations npx typeorm migration:run -d ./data-source.ts # roll back the last one npx typeorm migration:revert -d ./data-source.ts
Always review a generated migration before running it. Auto-generation is a starting point, not gospel — it can produce destructive or out-of-order changes. Read the up() and down(), especially for anything that drops or renames.

Seeding data

Seeding populates the database with initial data: reference tables (roles, categories), an admin user, or sample data for testing. A clean approach is a standalone script using the application context from the lifecycle lesson:

import { NestFactory } from '@nestjs/core'; async function seed() { const app = await NestFactory.createApplicationContext(AppModule); const usersService = app.get(UsersService); await usersService.create({ name: 'Admin', email: 'admin@app.com' }); await app.close(); } seed();
Make seeders idempotent. Check whether a record already exists before inserting (e.g. find-or-create). Then running the seeder twice does not create duplicates — safe to run on every deploy.

Migrations vs seeding

Keep them separate: migrations change the schema (structure), seeders insert data (content). Mixing data inserts into schema migrations makes both harder to reason about and to roll back.

Summary

Use migrations — versioned up/down scripts — for all production schema changes instead of synchronize. Generate them from entity diffs, but always review before running. Seed initial and reference data with idempotent scripts, ideally via the standalone application context. Keep schema (migrations) and data (seeders) concerns separate. Next: transactions, for changes that must all succeed or all fail.