Skip to content
Node.js nd libraries 5 min read

Sequelize: SQL ORM

Sequelize is a mature, promise-based ORM for Node.js that maps JavaScript classes to SQL tables and lets you query relational databases without writing raw SQL by hand. It speaks PostgreSQL, MySQL, MariaDB, SQLite, and SQL Server through a single API, so the same model code runs against different dialects. This page covers connecting, defining models, declaring associations, querying, running migrations, and wrapping writes in transactions.

Installing and connecting

Sequelize runs on any maintained Node.js release; Node 20 or 22 LTS is the sensible default. Install the core package plus the driver for your database — Sequelize does not bundle drivers, so you pick exactly one.

npm install sequelize
npm install pg pg-hstore   # PostgreSQL
# npm install mysql2       # MySQL / MariaDB
# npm install sqlite3      # SQLite

A Sequelize instance represents one database connection pool. Construct it from a connection URL (typically read from the environment) and call authenticate() to verify the credentials before your app starts serving traffic.

import { Sequelize } from "sequelize";

export const sequelize = new Sequelize(process.env.DATABASE_URL, {
  dialect: "postgres",
  logging: false,
});

await sequelize.authenticate();
console.log("Database connection established.");

Output:

Database connection established.

Sequelize maintains an internal connection pool. Create exactly one Sequelize instance per process and reuse it everywhere — instantiating a new one per request exhausts database connections fast.

Defining models

A model is a class that maps to a table. Each attribute declares a column with a DataTypes type and optional constraints such as allowNull, unique, or defaultValue. You can extend Model and call init, or use the shorthand sequelize.define. Sequelize adds id, createdAt, and updatedAt columns automatically.

import { DataTypes, Model } from "sequelize";
import { sequelize } from "./db.js";

export class User extends Model {}

User.init(
  {
    email: { type: DataTypes.STRING, allowNull: false, unique: true },
    name: { type: DataTypes.STRING },
  },
  { sequelize, modelName: "user" },
);

export class Post extends Model {}

Post.init(
  {
    title: { type: DataTypes.STRING, allowNull: false },
    published: { type: DataTypes.BOOLEAN, defaultValue: false },
  },
  { sequelize, modelName: "post" },
);

In development you can call sequelize.sync() to create tables from your models, but treat that as a prototyping shortcut — production schema changes belong in migrations (covered below).

Associations

Associations describe how tables relate and generate the helper methods and join logic for you. Declare them in both directions so Sequelize knows how to eager-load and how to attach foreign keys.

User.hasMany(Post, { foreignKey: "authorId", as: "posts" });
Post.belongsTo(User, { foreignKey: "authorId", as: "author" });

The common association types map directly to relational shapes:

MethodRelationshipForeign key lives on
hasOneOne-to-oneThe target model
belongsToOne-to-one / many-to-oneThe source model
hasManyOne-to-manyThe target model
belongsToManyMany-to-manyA join table (via through)

With the association in place, you can pull related rows in a single query using include.

Querying

Sequelize models expose finder methods (findAll, findOne, findByPk, create, update, destroy) that return promises. Filters use a plain options object; the where clause supports operators imported from the Op symbol for ranges, LIKE, IN, and boolean logic.

import { Op } from "sequelize";

await User.create({ email: "[email protected]", name: "Ada" });

const users = await User.findAll({
  where: { name: { [Op.like]: "A%" } },
  include: [{ model: Post, as: "posts" }],
  order: [["createdAt", "DESC"]],
  limit: 10,
});

console.log(`${users.length} user(s):`, users[0].email);

Output:

1 user(s): [email protected]

Because include joins the posts association, each returned user carries a populated user.posts array without a second round-trip.

Migrations

Models describe the current shape of your data, but migrations are the versioned, ordered scripts that evolve the real database — and they are what you run in CI and production. The Sequelize CLI scaffolds and runs them.

npm install --save-dev sequelize-cli
npx sequelize-cli migration:generate --name create-users
npx sequelize-cli db:migrate          # apply pending migrations
npx sequelize-cli db:migrate:undo     # roll back the last one

Each migration exports up (apply) and down (revert) functions that receive a queryInterface.

export async function up(queryInterface, Sequelize) {
  await queryInterface.createTable("users", {
    id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
    email: { type: Sequelize.STRING, allowNull: false, unique: true },
    name: { type: Sequelize.STRING },
    createdAt: { type: Sequelize.DATE, allowNull: false },
    updatedAt: { type: Sequelize.DATE, allowNull: false },
  });
}

export async function down(queryInterface) {
  await queryInterface.dropTable("users");
}

Never use sequelize.sync({ force: true }) against a real database — it drops and recreates tables, destroying data. Use migrations for any environment whose data you care about.

Transactions

When several writes must succeed or fail together, wrap them in a transaction. The managed form passes a callback: if it throws, Sequelize rolls everything back automatically; if it resolves, the work commits. Pass the transaction object to every query inside.

await sequelize.transaction(async (t) => {
  const user = await User.create(
    { email: "[email protected]", name: "Grace" },
    { transaction: t },
  );

  await Post.create(
    { title: "Hello", authorId: user.id },
    { transaction: t },
  );
});

If the second create fails, the first is undone — the database never ends up with a user who is missing their first post.

Best Practices

  • Create one Sequelize instance per process and share it; the pool handles concurrency for you.
  • Keep the connection string and credentials in environment variables, never in source.
  • Use migrations for every non-throwaway environment; reserve sync for quick local prototypes.
  • Declare associations on both models and prefer include over manual joins or extra queries.
  • Wrap multi-step writes in sequelize.transaction so partial failures roll back cleanly.
  • Import operators from Op instead of building raw SQL strings, which avoids injection and dialect quirks.
  • Set logging: false (or a custom logger) in production to keep noisy SQL out of your logs.
Last updated June 14, 2026
Was this helpful?