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:
| Command | Purpose |
|---|---|
| migrate | Run all pending migrations |
| migrate:rollback | Undo the last migration |
| migrate:create | Generate 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.