Sequelize Migrations with TypeScript and Umzug

How to Use Sequelize Migrations with TypeScript and Umzug

Still using sequelize.sync() to create your tables?

That approach works well for quick prototypes, but in real-world applications it becomes difficult to maintain. What happens when you need to:

  • Change a column
  • Add an index
  • Roll back a bad schema update
  • Track database history

If you rely on sync(), you often end up dropping tables and recreating them, which can be risky and impractical.

A better solution is database migrations.

In this guide, you’ll learn how to implement Sequelize migrations in a TypeScript project using Umzug, a flexible migration framework for Node.js.

🚀 Complete JavaScript Guide (Beginner + Advanced)

🚀 NodeJS – The Complete Guide (MVC, REST APIs, GraphQL, Deno)


Why sequelize.sync() Is Not Enough

In development, Sequelize can automatically create tables from models using:

sequelize.sync()

However, this approach has limitations:

  • No version control for schema changes
  • Difficult to roll back updates
  • Risky in production
  • Hard to collaborate in teams

Instead, professional projects use migrations.


What Are Database Migrations?

Database migrations are version-controlled scripts that modify your database schema.

Each migration includes two functions:

  • up → apply changes
  • down → revert changes

Example workflow:

State A → Migration → State B
State B → Rollback → State A

Because migrations live in your codebase, they are tracked with Git and run sequentially.


Why Use Umzug for TypeScript Migrations?

Sequelize includes its own CLI for migrations, but TypeScript support is limited.

Instead, we can use Umzug, a migration framework that works perfectly with TypeScript.

Benefits of Umzug:

  • Framework-agnostic
  • Works with Sequelize
  • Clean migration API
  • Easy rollback support

Install Dependencies

Install Umzug and TS Node:

npm install umzug ts-node

We’ll run migrations directly with TypeScript, without compiling them first.


Create the Migration Entry File

Create a file in the project root:

migrate.js

Add the following code:

require("ts-node/register")
require("./src/umzug")

This registers TS Node and loads the Umzug configuration.

ESLint Fix

ESLint may complain about require.

Disable the rule in eslint.config.mjs:

rules: {
"@typescript-eslint/no-var-requires": "off"
}

Create the Umzug Configuration

Create a new file:

src/umzug.ts

Example configuration:

import { Umzug, SequelizeStorage } from "umzug"
import { Sequelize } from "sequelize"
import config from "./config"const sequelize = new Sequelize(
config.database,
config.username,
config.password,
{
host: config.host,
dialect: "mysql"
}
)export const migrator = new Umzug({
migrations: {
glob: `${__dirname}/data/migrations/*.ts`
},
context: sequelize,
storage: new SequelizeStorage({
sequelize
}),
logger: console
})export type Migration = typeof migrator._types.migration

What This Configuration Does

  • Loads migrations from src/data/migrations
  • Uses Sequelize as the context
  • Stores migration history in the database
  • Logs migration activity

Add Migration Scripts

Add scripts to your package.json:

{
"scripts": {
"migrate": "node migrate up",
"migrate:rollback": "node migrate down",
"migrate:create": "node migrate create"
}
}

These commands allow you to:

CommandPurpose
migrateRun all pending migrations
migrate:rollbackUndo the last migration
migrate:createGenerate a new migration

Create Your First Migration

Run:

npm run migrate:create users-table

If the migrations folder does not exist, specify the path:

src/data/migrations

Then replace the generated file with this code.


Example Sequelize Migration

import { DataTypes, Sequelize } from "sequelize"
import { Migration } from "../../umzug"export const up: Migration = async ({ context: sequelize }) => {
const queryInterface = sequelize.getQueryInterface() await queryInterface.createTable("users", {
id: {
type: DataTypes.BIGINT,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
createdAt: {
type: DataTypes.DATE
},
updatedAt: {
type: DataTypes.DATE
}
})
}export const down: Migration = async ({ context: sequelize }) => {
const queryInterface = sequelize.getQueryInterface() await queryInterface.dropTable("users")
}

Run the Migration

Before running migrations, remove any existing:

sequelize.sync()

from your application startup code.

Then run:

npm run migrate

This will create the users table.


Rolling Back a Migration

Let’s say you forgot to make the email column unique.

No problem.

Run:

npm run migrate:rollback

This executes the down() function and reverts the schema.

Then update the migration and run:

npm run migrate

This iterative workflow makes schema design much safer and easier.


Should Migrations Be Compiled?

Because we run migrations with TS Node, they do not need to be compiled.

You can exclude them from tsconfig.json:

{
"exclude": [
"src/umzug.ts",
"src/data/migrations"
]
}

This keeps your dist directory clean.


Sequelize Models vs Migrations

This is a common question.

Sequelize Models

Models define:

  • Data structure
  • Types
  • Validations
  • Relationships

They are used at runtime to interact with the database.

Example:

User.init({
name: DataTypes.STRING,
email: DataTypes.STRING
})

Sequelize Migrations

Migrations define database schema changes:

  • Creating tables
  • Adding columns
  • Creating indexes
  • Modifying schema

They ensure your database evolves safely over time.

Important:

Sequelize does not automatically sync models with migrations, so you must keep them consistent manually.


Final Thoughts

Using migrations instead of sequelize.sync() gives you:

  • Version-controlled schema changes
  • Safer deployments
  • Easy rollbacks
  • Better collaboration in teams

By combining Sequelize + TypeScript + Umzug, you get a powerful and flexible migration workflow.

Share this article

Similar Posts