Sequelize Migrations — Complete Guide
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
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