Skip to content

Sequelize Migrations — Complete Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Sequelize Migrations. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Sequelize migrations: use Sequelize CLI for generating and running migrations, define models with migrations, handle associations, seed data, and integrate Sequelize migrations with Node.js applications.

What You Learn

You will learn how to use Sequelize for database migrations: install and configure Sequelize, generate migrations from model changes, manage associations through migrations, seed databases, and integrate migrations with Node.js applications.

Why It Matters

Sequelize is a popular Node.js ORM with built-in migration support. If you use Sequelize as your ORM, using its migration system keeps everything in the Sequelize ecosystem. Migrations are defined using the same model definitions as your application code.

Real-World Use

DodaTech's Node.js content management system uses Sequelize with PostgreSQL. Migrations manage 40+ tables with complex associations. The Sequelize CLI generates migrations from model changes, and the migration runner integrates with the deployment pipeline.

Installation and Setup

# Install Sequelize and CLI
npm install sequelize
npm install sequelize-cli
npm install pg pg-hstore  # PostgreSQL driver

# Initialize Sequelize project
npx sequelize-cli init

# Creates:
# config/config.json
# models/index.js
# migrations/
# seeders/
// config/config.json
{
    "development": {
        "username": "postgres",
        "password": null,
        "database": "app_dev",
        "host": "127.0.0.1",
        "dialect": "postgres"
    },
    "test": {
        "username": "postgres",
        "password": null,
        "database": "app_test",
        "host": "127.0.0.1",
        "dialect": "postgres"
    },
    "production": {
        "username": process.env.DB_USERNAME,
        "password": process.env.DB_PASSWORD,
        "database": process.env.DB_NAME,
        "host": process.env.DB_HOST,
        "dialect": "postgres",
        "dialectOptions": {
            "ssl": { "require": true, "rejectUnauthorized": false }
        }
    }
}

Expected output: sequelize-cli init creates the project structure. Configuration file stores database connection details per environment. Always use environment variables for production credentials.

Generating Migrations

# Generate a migration
npx sequelize-cli migration:generate --name create-users-table

# Output:
# migrations/20260628120000-create-users-table.js
// migrations/20260628120000-create-users-table.js
module.exports = {
    async up(queryInterface, Sequelize) {
        await queryInterface.createTable('Users', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER,
            },
            name: {
                type: Sequelize.STRING(100),
                allowNull: false,
            },
            email: {
                type: Sequelize.STRING(255),
                allowNull: false,
                unique: true,
            },
            phone: {
                type: Sequelize.STRING(20),
                allowNull: true,
            },
            isActive: {
                type: Sequelize.BOOLEAN,
                defaultValue: true,
                field: 'is_active', // Map to snake_case in DB
            },
            createdAt: {
                allowNull: false,
                type: Sequelize.DATE,
                defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
            },
            updatedAt: {
                allowNull: false,
                type: Sequelize.DATE,
                defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
            },
        });

        await queryInterface.addIndex('Users', ['email', 'isActive'], {
            name: 'idx_users_email_active',
        });
    },

    async down(queryInterface, Sequelize) {
        await queryInterface.dropTable('Users');
    },
};

Expected output: Migration defines columns with types, constraints, and defaults. queryInterface methods create tables, indexes, and constraints. The down method drops the table.

Model Migration Pattern

// models/user.js - Sequelize model
module.exports = (sequelize, DataTypes) => {
    const User = sequelize.define('User', {
        name: DataTypes.STRING(100),
        email: {
            type: DataTypes.STRING(255),
            unique: true,
            allowNull: false,
        },
        phone: DataTypes.STRING(20),
        isActive: {
            type: DataTypes.BOOLEAN,
            defaultValue: true,
        },
    }, {
        tableName: 'Users',
        underscored: true, // Use snake_case in DB
    });

    User.associate = (models) => {
        User.hasMany(models.Order, { foreignKey: 'userId' });
    };

    return User;
};

// The migration and model should match.
// Changes to the model require a new migration.

Expected output: Model definition mirrors the migration. The model maps to the same table and columns. Underscored option maps camelCase model attributes to snake_case database columns.

Associations in Migrations

// migrations/20260628130000-create-orders-table.js
module.exports = {
    async up(queryInterface, Sequelize) {
        await queryInterface.createTable('Orders', {
            id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER,
            },
            userId: {
                type: Sequelize.INTEGER,
                allowNull: false,
                references: {
                    model: 'Users',
                    key: 'id',
                },
                onUpdate: 'CASCADE',
                onDelete: 'CASCADE',
            },
            total: {
                type: Sequelize.DECIMAL(10, 2),
                allowNull: false,
            },
            status: {
                type: Sequelize.ENUM('pending', 'paid', 'shipped', 'delivered'),
                defaultValue: 'pending',
            },
            createdAt: {
                allowNull: false,
                type: Sequelize.DATE,
            },
            updatedAt: {
                allowNull: false,
                type: Sequelize.DATE,
            },
        });

        await queryInterface.addIndex('Orders', ['userId'], {
            name: 'idx_orders_user',
        });
    },

    async down(queryInterface, Sequelize) {
        await queryInterface.dropTable('Orders');
    },
};

Expected output: Foreign key is defined in the migration using references. Sequelize manages the constraint creation. The model's associate method mirrors this relationship. Indexes on foreign key columns improve join performance.

Adding Columns

// migrations/20260628140000-add-phone-to-users.js
module.exports = {
    async up(queryInterface, Sequelize) {
        await queryInterface.addColumn('Users', 'phone', {
            type: Sequelize.STRING(20),
            allowNull: true,
        });
    },

    async down(queryInterface, Sequelize) {
        await queryInterface.dropColumn('Users', 'phone');
    },
};
// migrations/20260628150000-add-role-to-users.js
module.exports = {
    async up(queryInterface, Sequelize) {
        await queryInterface.addColumn('Users', 'role', {
            type: Sequelize.ENUM('admin', 'user', 'moderator'),
            defaultValue: 'user',
            allowNull: false,
        });
    },

    async down(queryInterface, Sequelize) {
        // Remove ENUM type first, then column
        await queryInterface.removeColumn('Users', 'role');
        await queryInterface.sequelize.query(
            'DROP TYPE IF EXISTS "enum_Users_role"'
        );
    },
};

Expected output: Add column migrations use queryInterface.addColumn. The down method removes the column. For ENUM columns, the down method must also drop the ENUM type.

Seeding

// seeders/20260628120000-demo-users.js
module.exports = {
    async up(queryInterface, Sequelize) {
        await queryInterface.bulkInsert('Users', [
            {
                name: 'Alice Johnson',
                email: 'alice@example.com',
                isActive: true,
                createdAt: new Date(),
                updatedAt: new Date(),
            },
            {
                name: 'Bob Smith',
                email: 'bob@example.com',
                isActive: true,
                createdAt: new Date(),
                updatedAt: new Date(),
            },
        ]);
    },

    async down(queryInterface, Sequelize) {
        await queryInterface.bulkDelete('Users', null, {});
    },
};

Expected output: Seed files populate tables with test data. The up method inserts data. The down method deletes it. Seeds are run with npx sequelize-cli db:seed:all.

Running Migrations

# Run all pending migrations
npx sequelize-cli db:migrate

# Undo the last migration
npx sequelize-cli db:migrate:undo

# Undo all migrations
npx sequelize-cli db:migrate:undo:all

# Run seeds
npx sequelize-cli db:seed:all

# Undo seeds
npx sequelize-cli db:seed:undo:all

# Show migration status
npx sequelize-cli db:migrate:status

Expected output: Sequelize CLI manages the migration lifecycle. db:migrate applies all pending migrations. db:migrate:undo reverts the last one. Seeds populate test data independently of migrations.

Integration with Express

// app.js
const express = require('express');
const { sequelize } = require('./models');

const app = express();

async function start() {
    try {
        // Run migrations
        await sequelize.sync(); // Note: sync != migrations
        // For production, use: npx sequelize-cli db:migrate

        app.listen(3000, () => {
            console.log('Server running on port 3000');
        });
    } catch (err) {
        console.error('Failed to start:', err);
        process.exit(1);
    }
}

start();

// Use sequelize.sync({ alter: true }) only in development.
// In production, always use explicit migrations via CLI.

Expected output: sequelize.sync() syncs models in development. Production uses CLI migrations for controlled schema changes. Never use sync({ alter: true }) in production as it may drop columns or data.

Common Mistakes

1. Using sync() in Production

sequelize.sync({ alter: true }) alters tables to match models automatically. This is dangerous in production as it may drop columns or change types unexpectedly. Always use explicit migrations in production.

2. Forgetting to Generate Migrations After Model Changes

Changing a model without generating a corresponding migration causes mismatch. Always run migration:generate after model changes. The migration and model must stay in sync.

3. Not Handling ENUM Type Changes

Sequelize ENUM types are database-level types. Changing ENUM values requires dropping and recreating the type, which is complex. Use VARCHAR with validation instead of ENUM for values that may change.

4. Hardcoding Configuration

config/config.json with hardcoded credentials is a security risk. Use JSON with environment variable interpolation or switch to a JS config file that reads Process.env.

5. No Down Migration for ENUM Columns

Dropping an ENUM column requires dropping the ENUM type separately. Without this, the ENUM type persists in the database. Always clean up ENUM types in the down migration.

Practice Questions

1. How does Sequelize track applied migrations?

Sequelize creates a SequelizeMeta table (configurable) storing migration names. On each run, it checks this table to determine which migrations have been applied and runs only pending ones.

2. What is the difference between sequelize.sync() and migrations?

sync() creates tables from model definitions automatically. It is convenient for development but dangerous for production. Migrations are explicit, version-controlled, and reversible. Always use migrations in production.

3. How do you handle associations in Sequelize migrations?

Define foreign key columns in the migration using references. The model's associate() method mirrors the relationship. Ensure both the migration and model define matching associations.

4. Why should you avoid using sync({ alter: true }) in production?

alter: true automatically adds, removes, or modifies columns to match models. It may drop columns with data, change types incompatibly, or make unexpected changes. Migrations give you explicit control.

Challenge

Set up Sequelize with: PostgreSQL database, Users and Orders models with associations, migrations for both tables with foreign keys, add-column migration for Users, seed files for test data, and Express integration that runs CLI migrations on deployment (not sync).

FAQ

Should I use Sequelize migrations or raw SQL migrations?

If you use Sequelize as your ORM, use Sequelize migrations. They keep everything in the same ecosystem. If you use Knex or raw SQL, use those migration tools. Consistency matters more than the tool choice.

How do I handle migration conflicts in team development?

Run migrations sequentially. Use timestamp-based naming. Never edit applied migrations. If two developers create conflicting migrations, resolve during merge by re-ordering or creating a merge migration.

Can I rollback a Sequelize migration that failed?

Sequelize wraps each migration in a transaction. If a migration fails, the transaction rolls back. The migration is not marked as applied. Fix the issue and run migrations again.

How do I seed data for testing?

Use seed files in the seeders directory. Run npx sequelize-cli db:seed:all to populate data. Use db:seed:undo:all to clean up before tests.

What is the best practice for ENUM types in Sequelize?

Avoid ENUM types for values that may change. Use VARCHAR with validation in the model instead. ENUM changes require complex migrations. VARCHAR changes are simple add/remove column operations.

Mini Project: Sequelize Migration Setup

Set up Sequelize for a Node.js application with: PostgreSQL configuration, Users and Posts models with associations, migrations for both tables, add-column migration (bio to Users), seed data for development, and CLI integration with npm scripts (migrate, rollback, seed).

What's Next

Now that you understand Sequelize migrations, explore Django Migrations for the Django ORM migration approach.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro