Blog/How to Use Sequelize with Node.js and MySQL (Models, Migrations, Associations, and Queries)

Node.jsSequelizeMySQLORMJavaScriptDevOps··greencrew

How to Use Sequelize with Node.js and MySQL (Models, Migrations, Associations, and Queries)

To use Sequelize with Node.js and MySQL, run npm install sequelize mysql2, create a connection with new Sequelize(database, user, password, { host: 'localhost', dialect: 'mysql' }), check it with sequelize.authenticate(), and define models with sequelize.define(). This guide covers creating the MySQL user, sequelize-cli migrations and seeders, CRUD queries, operators, associations, transactions, and common connection errors.

Short answer: Install the ORM and the MySQL driver with npm install sequelize mysql2. Connect with new Sequelize('appdb', 'appuser', 'password', { host: 'localhost', dialect: 'mysql' }) and verify the connection with await sequelize.authenticate(). Define tables as models (sequelize.define('Book', { title: DataTypes.STRING })), and manage schema changes with sequelize-cli migrations (npx sequelize-cli db:migrate) instead of sync() in production. Query with Book.findAll({ where: { ... } }), link tables with hasMany/belongsTo, and wrap multi-step writes in sequelize.transaction().

What Is Sequelize?

Sequelize is a promise-based ORM (Object-Relational Mapper) for Node.js. It maps database tables to JavaScript classes, so you write Book.findByPk(1) instead of raw SQL. It supports MySQL, MariaDB, PostgreSQL, SQLite, and Microsoft SQL Server. This guide uses Sequelize v6, the stable release line documented at sequelize.org/docs/v6. The next major is published separately as @sequelize/core.

ConceptWhat it isExample
InstanceThe connection and its poolnew Sequelize(...)
ModelA tableBook → Books table
AttributeA columntitle: DataTypes.STRING
MigrationA versioned schema change20261028-create-book.js
SeederSample or reference data20261028-demo-books.js
AssociationA relationship between tablesAuthor.hasMany(Book)

Prerequisites

Step 1: Create the MySQL Database and User

Never connect your app as MySQL root. Create a dedicated user:

sudo mysql <<'SQL'
CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'change-this-strong-password';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
SQL

Step 2: Install Sequelize and Connect to MySQL

mkdir books-api && cd books-api
npm init -y
npm install sequelize mysql2 dotenv

mysql2 is the driver Sequelize uses for MySQL. Use mariadb with dialect: 'mariadb' if you're on MariaDB and prefer its native driver.

Create .env (and add it to .gitignore):

DB_HOST=127.0.0.1
DB_NAME=appdb
DB_USER=appuser
DB_PASS=change-this-strong-password

Create db.js:

require('dotenv').config();
const { Sequelize } = require('sequelize');

const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, {
  host: process.env.DB_HOST,
  dialect: 'mysql',
  logging: process.env.NODE_ENV === 'production' ? false : console.log,
  pool: { max: 10, min: 0, acquire: 30000, idle: 10000 },
});

module.exports = sequelize;

Test it:

// check-db.js
const sequelize = require('./db');

(async () => {
  try {
    await sequelize.authenticate();
    console.log('Connected to MySQL');
  } catch (err) {
    console.error('Unable to connect:', err.message);
  } finally {
    await sequelize.close();
  }
})();
node check-db.js
Pool optionMeaning
maxMaximum open connections per Node process. Multiply by the number of processes when you cluster, and stay below MySQL's max_connections.
minConnections kept open when idle
acquireMilliseconds to wait for a free connection before throwing
idleMilliseconds before an idle connection is released

Step 3: Define a Model

// models/book.js
const { DataTypes } = require('sequelize');
const sequelize = require('../db');

const Book = sequelize.define('Book', {
  title: { type: DataTypes.STRING, allowNull: false },
  author: { type: DataTypes.STRING, allowNull: false },
  releaseDate: { type: DataTypes.DATEONLY },
  subject: { type: DataTypes.INTEGER },
}, {
  tableName: 'books',     // otherwise Sequelize pluralizes to "Books"
  underscored: true,      // created_at / release_date column names
});

module.exports = Book;
OptionDefaultEffect
timestampstrueAdds and manages createdAt / updatedAt
tableNamePluralized model nameExact table name
freezeTableNamefalseUse the model name as-is
underscoredfalsesnake_case column names
paranoidfalseSoft deletes with deletedAt (needs timestamps)

Common data types: STRING (VARCHAR 255), TEXT, INTEGER, BIGINT, DECIMAL(10, 2), BOOLEAN, DATE (DATETIME), DATEONLY, JSON, and ENUM('draft', 'published').

sync() vs Migrations

await sequelize.sync() creates missing tables from your models, which is handy for a quick prototype. sync({ alter: true }) and sync({ force: true }) change or drop tables to match the models. Never run those against production data. For anything real, use migrations.

Step 4: Manage the Schema with sequelize-cli Migrations

npm install --save-dev sequelize-cli
npx sequelize-cli init

This creates config/config.json, models/, migrations/, and seeders/. Swap the JSON config for a JS file so it reads environment variables. Create .sequelizerc:

const path = require('node:path');
module.exports = { config: path.resolve('config', 'config.js') };
// config/config.js
require('dotenv').config();
const base = {
  username: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  host: process.env.DB_HOST,
  dialect: 'mysql',
};
module.exports = { development: base, test: base, production: { ...base, logging: false } };

Generate a model and its migration:

npx sequelize-cli model:generate --name Author --attributes name:string,country:string
npx sequelize-cli model:generate --name Book --attributes title:string,releaseDate:dateonly,authorId:integer

Add a foreign key to the generated Book migration's authorId column:

authorId: {
  type: Sequelize.INTEGER,
  allowNull: false,
  references: { model: 'Authors', key: 'id' },
  onDelete: 'CASCADE',
},

Run and inspect migrations:

CommandWhat it does
npx sequelize-cli db:migrateApply pending migrations
npx sequelize-cli db:migrate:statusShow applied and pending migrations
npx sequelize-cli db:migrate:undoRoll back the last migration
npx sequelize-cli migration:generate --name add-isbn-to-booksEmpty migration for a schema change
npx sequelize-cli seed:generate --name demo-booksNew seeder file
npx sequelize-cli db:seed:allRun all seeders
NODE_ENV=production npx sequelize-cli db:migrateUse the production config block

Sequelize records applied migrations in a SequelizeMeta table, so each one runs exactly once per database. Run db:migrate as a deploy step before restarting the app.

Step 5: Query Data (CRUD)

The CLI-generated models/index.js loads every model and runs its associate function. With the models from Step 4:

const { Author, Book } = require('./models');
const { Op } = require('sequelize');

// Create
const author = await Author.create({ name: 'Ursula K. Le Guin', country: 'US' });
await Book.create({ title: 'The Dispossessed', releaseDate: '1974-05-01', authorId: author.id });

// Read
const all = await Book.findAll();
const one = await Book.findByPk(1);
const recent = await Book.findAll({
  where: { releaseDate: { [Op.gte]: '1970-01-01' } },
  order: [['releaseDate', 'DESC']],
  limit: 10,
  attributes: ['id', 'title', 'releaseDate'],
});

// Update
await Book.update({ title: 'The Dispossessed: An Ambiguous Utopia' }, { where: { id: 1 } });

// Delete
await Book.destroy({ where: { id: 1 } });
OperatorSQLExample
Op.eq / Op.ne= / !={ status: { [Op.ne]: 'draft' } }
Op.gt, Op.gte, Op.lt, Op.lte>, >=, <, <={ price: { [Op.lt]: 20 } }
Op.inIN (...){ id: { [Op.in]: [1, 2, 3] } }
Op.likeLIKE{ title: { [Op.like]: '%dragon%' } }
Op.betweenBETWEEN{ year: { [Op.between]: [1960, 1980] } }
Op.orOR{ [Op.or]: [{ a: 1 }, { b: 2 }] }

Paginate with findAndCountAll({ limit, offset }). When you need raw SQL, use bound parameters, never string concatenation: sequelize.query('SELECT * FROM Books WHERE id = ?', { replacements: [id], type: QueryTypes.SELECT }).

Step 6: Add Associations

In models/author.js and models/book.js, fill in the generated associate methods:

// models/author.js
static associate(models) {
  Author.hasMany(models.Book, { foreignKey: 'authorId', as: 'books' });
}

// models/book.js
static associate(models) {
  Book.belongsTo(models.Author, { foreignKey: 'authorId', as: 'author' });
}

Load related rows with include (an SQL JOIN):

const authors = await Author.findAll({
  include: [{ model: Book, as: 'books', attributes: ['title'] }],
});
RelationshipMethodsForeign key lives on
One-to-onehasOne + belongsToThe belongsTo side
One-to-manyhasMany + belongsToThe "many" side
Many-to-manybelongsToMany on both, { through: 'BookGenres' }A junction table

Loading associations inside a loop (for (const a of authors) await a.getBooks()) runs one query per row. Use include to load everything in one query.

Step 7: Use Transactions for Multi-Step Writes

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

await sequelize.transaction(async (t) => {
  const author = await Author.create({ name: 'N. K. Jemisin' }, { transaction: t });
  await Book.create({ title: 'The Fifth Season', authorId: author.id }, { transaction: t });
}); // commits if the callback resolves, rolls back if it throws

Pass { transaction: t } to every query inside the callback. A query without it runs outside the transaction.

Troubleshooting Sequelize and MySQL

SymptomCauseFix
Please install mysql2 package manuallyDriver not installednpm install mysql2
SequelizeConnectionRefusedError: connect ECONNREFUSED 127.0.0.1:3306MySQL not running or listening elsewheresudo systemctl status mysql, check host and port
SequelizeAccessDeniedError: Access denied for userWrong password, or user created for another hostRecreate 'appuser'@'localhost'. Match DB_HOST.
Unknown column 'createdAt' in 'field list'Table created without timestampsAdd the columns in a migration, or timestamps: false
Table 'appdb.Books' doesn't existPluralized table name vs actual nameSet tableName, or run your migrations
SequelizeConnectionAcquireTimeoutErrorPool exhausted (slow queries or leaked transactions)Raise pool.max carefully, fix slow queries, always finish transactions
ER_CON_COUNT_ERROR: Too many connectionspool.max × processes > max_connectionsLower pool.max or raise MySQL's limit
Migration ran in the wrong databaseNODE_ENV not setNODE_ENV=production npx sequelize-cli db:migrate
Emoji saved as ????Not using utf8mb4Create the database and tables with utf8mb4

Monitor the API and Its Database Connection

Most Sequelize outages show up as a hung pool or refused connections, and the Node process stays up while every request fails. Add a health route that actually touches the database:

app.get('/health', async (req, res) => {
  try {
    await sequelize.query('SELECT 1');
    res.json({ status: 'ok' });
  } catch {
    res.status(503).json({ status: 'db-down' });
  }
});

Then point GreenCrew at it. GreenCrew runs HTTP uptime checks and alerts you on Discord, Slack, or Telegram when the endpoint stops responding correctly, with a public status page for your users. See how to get website downtime alerts on Discord, Slack, and Telegram.

Frequently Asked Questions

Should I use sequelize.sync() in production?

No. sync() can't express renames or data changes, and alter/force can drop data. Use sequelize-cli migrations so every schema change is versioned, reviewed, and reversible.

Does Sequelize work with MariaDB?

Yes. Use dialect: 'mariadb' with the mariadb driver package, or keep dialect: 'mysql' with mysql2, which also works against MariaDB for most apps.

How do I prevent SQL injection with Sequelize?

Model methods (findAll, create, update with where) escape values for you. With sequelize.query, always use replacements or bind parameters, and never interpolate user input into the SQL string.

Sequelize or Prisma or TypeORM?

All three work with MySQL. Sequelize is mature, JavaScript-first, and has a large ecosystem. Prisma uses a schema file and a generated, strongly typed client. TypeORM fits decorator-based TypeScript projects such as NestJS. Pick the one that matches your codebase and team.

Conclusion

Using Sequelize with Node.js and MySQL comes down to a few steps. Install sequelize and mysql2, connect with a dedicated MySQL user loaded from environment variables, define models, and evolve the schema with sequelize-cli migrations instead of sync(). Use operators and include for queries, transactions for multi-step writes, and size your pool for the number of Node processes. Then deploy the API behind PM2 and Nginx or Docker, and let GreenCrew watch a database-backed /health route.