← Back to DevBytes

Sequelize from Beginner to Expert: A Learning Path

Sequelize from Beginner to Expert: A Learning Path

Sequelize is a promise-based Node.js Object-Relational Mapper (ORM) for Postgres, MySQL, MariaDB, SQLite, and Microsoft SQL Server. It provides a high-level abstraction over raw SQL, allowing developers to interact with relational databases using JavaScript objects and methods. Whether you are building a small REST API or a large-scale enterprise application, Sequelize can dramatically reduce boilerplate code while keeping your data layer organized and maintainable.

Why Sequelize Matters

Writing raw SQL queries in Node.js applications quickly becomes unwieldy as the project grows. You end up with string concatenation, manual parameter binding, and duplicated logic across handlers. Sequelize solves these problems by mapping database tables to JavaScript classes (models), giving you type-safe access, automatic validation, and a consistent API across multiple database engines.

1. Installation and Setup

Start by initializing a Node.js project and installing Sequelize along with the database driver of your choice. In this tutorial, we will use PostgreSQL.

mkdir sequelize-tutorial && cd sequelize-tutorial
npm init -y
npm install sequelize pg pg-hstore
npm install --save-dev sequelize-cli

Initialize the Sequelize project structure. This generates folders for configuration, migrations, models, and seeders.

npx sequelize-cli init

Your project structure will look like this:

sequelize-tutorial/
├── config/
│   └── config.json
├── models/
│   └── index.js
├── migrations/
├── seeders/
└── package.json

Update config/config.json with your database credentials:

{
  "development": {
    "username": "postgres",
    "password": "your_password",
    "database": "sequelize_dev",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "test": {
    "username": "postgres",
    "password": "your_password",
    "database": "sequelize_test",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "production": {
    "use_env_variable": "DATABASE_URL",
    "dialect": "postgres"
  }
}

2. Connecting to the Database Programmatically

While the CLI handles configuration automatically, it is important to understand how to instantiate Sequelize manually. This is useful in custom server setups or when using environment variables directly.

const { Sequelize } = require('sequelize');

const sequelize = new Sequelize(
  process.env.DB_NAME || 'sequelize_dev',
  process.env.DB_USER || 'postgres',
  process.env.DB_PASSWORD || 'your_password',
  {
    host: process.env.DB_HOST || '127.0.0.1',
    dialect: 'postgres',
    logging: false,
    pool: {
      max: 5,
      min: 0,
      acquire: 30000,
      idle: 10000
    }
  }
);

async function testConnection() {
  try {
    await sequelize.authenticate();
    console.log('Connection has been established successfully.');
  } catch (error) {
    console.error('Unable to connect to the database:', error);
  }
}

testConnection();

module.exports = sequelize;

3. Defining Models

Models are the heart of Sequelize. A model represents a table in the database and provides an interface for querying. You can create a model using the CLI or programmatically.

Generate a model with the CLI:

npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,email:string

This creates two files: a model file in models/ and a migration file in migrations/. Alternatively, define a model manually:

const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');

const User = sequelize.define('User', {
  firstName: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      notEmpty: true,
      len: [2, 50]
    }
  },
  lastName: {
    type: DataTypes.STRING,
    allowNull: false
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true
    }
  },
  age: {
    type: DataTypes.INTEGER,
    defaultValue: 18,
    validate: {
      min: 0,
      max: 120
    }
  }
}, {
  tableName: 'users',
  timestamps: true,
  paranoid: true,
  underscored: true
});

module.exports = User;

Key options to note: timestamps adds createdAt and updatedAt columns, paranoid enables soft deletes by adding a deletedAt column, and underscored converts camelCase column names to snake_case in the database.

4. Running Migrations

Migrations are version-controlled scripts that modify the database schema. After generating a model, run the migration to create the table.

npx sequelize-cli db:migrate

If you need to undo a migration, use:

npx sequelize-cli db:migrate:undo
npx sequelize-cli db:migrate:undo:all

A typical migration file looks like this:

'use strict';

module.exports = {
  async up(queryInterface, DataTypes) {
    await queryInterface.createTable('users', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: DataTypes.INTEGER
      },
      first_name: {
        type: DataTypes.STRING,
        allowNull: false
      },
      last_name: {
        type: DataTypes.STRING,
        allowNull: false
      },
      email: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true
      },
      created_at: {
        allowNull: false,
        type: DataTypes.DATE
      },
      updated_at: {
        allowNull: false,
        type: DataTypes.DATE
      }
    });
  },

  async down(queryInterface, DataTypes) {
    await queryInterface.dropTable('users');
  }
};

5. CRUD Operations

Once your models are defined and migrated, you can perform Create, Read, Update, and Delete operations using Sequelize's intuitive API.

Create

const User = require('./models/user');

async function createUser() {
  const user = await User.create({
    firstName: 'Jane',
    lastName: 'Doe',
    email: 'jane@example.com',
    age: 28
  });
  console.log('User created:', user.toJSON());
}

createUser();

Read

async function getUsers() {
  // Find all users
  const allUsers = await User.findAll();
  
  // Find one user by primary key
  const userById = await User.findByPk(1);
  
  // Find one user by condition
  const userByEmail = await User.findOne({
    where: { email: 'jane@example.com' }
  });
  
  // Find with filtering, sorting, and pagination
  const filteredUsers = await User.findAll({
    where: { age: { [Op.gte]: 18 } },
    order: [['created_at', 'DESC']],
    limit: 10,
    offset: 0
  });
  
  return filteredUsers;
}

Update

async function updateUser(id) {
  const user = await User.findByPk(id);
  if (!user) throw new Error('User not found');
  
  await user.update({ firstName: 'Janet' });
  console.log('Updated user:', user.toJSON());
}

// Alternatively, update multiple records at once
async function bulkUpdate() {
  const [numberOfAffectedRows, affectedRows] = await User.update(
    { age: 21 },
    { where: { age: 18 }, returning: true }
  );
  console.log(`Updated ${numberOfAffectedRows} rows`);
}

Delete

async function deleteUser(id) {
  const user = await User.findByPk(id);
  if (!user) throw new Error('User not found');
  
  // Soft delete (because paranoid: true)
  await user.destroy();
  
  // To permanently delete
  await user.destroy({ force: true });
  
  // Restore a soft-deleted record
  await user.restore();
}

6. Associations

Sequelize supports four types of associations: hasOne, belongsTo, hasMany, and belongsToMany. Let's model a blog where a User has many Posts, and a Post belongs to a User.

// models/user.js
const User = sequelize.define('User', { /* ... */ });

// models/post.js
const Post = sequelize.define('Post', {
  title: {
    type: DataTypes.STRING,
    allowNull: false
  },
  content: {
    type: DataTypes.TEXT,
    allowNull: false
  },
  published: {
    type: DataTypes.BOOLEAN,
    defaultValue: false
  }
});

// Define associations
User.hasMany(Post, {
  foreignKey: 'userId',
  as: 'posts'
});

Post.belongsTo(User, {
  foreignKey: 'userId',
  as: 'author'
});

For a many-to-many relationship, such as Posts and Tags, use belongsToMany with a join table:

const Tag = sequelize.define('Tag', {
  name: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true
  }
});

const PostTag = sequelize.define('PostTag', {
  postId: {
    type: DataTypes.INTEGER,
    references: { model: Post, key: 'id' }
  },
  tagId: {
    type: DataTypes.INTEGER,
    references: { model: Tag, key: 'id' }
  }
});

Post.belongsToMany(Tag, { through: PostTag, as: 'tags' });
Tag.belongsToMany(Post, { through: PostTag, as: 'posts' });

Eager and Lazy Loading

// Eager loading: fetch user with their posts in one query
const userWithPosts = await User.findOne({
  where: { id: 1 },
  include: [{
    model: Post,
    as: 'posts',
    where: { published: true },
    required: false
  }]
});

// Lazy loading: fetch posts only when needed
const user = await User.findByPk(1);
const posts = await user.getPosts();

7. Advanced Querying

Sequelize provides the Op operator object for building complex WHERE clauses. Mastering these operators is essential for expert-level usage.

const { Op } = require('sequelize');

async function advancedQueries() {
  // AND / OR
  const users = await User.findAll({
    where: {
      [Op.and]: [
        { age: { [Op.gte]: 18 } },
        { age: { [Op.lte]: 65 } }
      ]
    }
  });

  // LIKE search
  const searchResults = await User.findAll({
    where: {
      lastName: { [Op.like]: '%son%' }
    }
  });

  // IN clause
  const specificUsers = await User.findAll({
    where: {
      id: { [Op.in]: [1, 2, 3, 5] }
    }
  });

  // Between dates
  const recentUsers = await User.findAll({
    where: {
      created_at: {
        [Op.between]: [new Date('2024-01-01'), new Date('2024-12-31')]
      }
    }
  });

  // Aggregations
  const stats = await User.findAll({
    attributes: [
      [sequelize.fn('COUNT', sequelize.col('id')), 'total_users'],
      [sequelize.fn('AVG', sequelize.col('age')), 'average_age']
    ],
    raw: true
  });

  // Group by
  const grouped = await Post.findAll({
    attributes: [
      'userId',
      [sequelize.fn('COUNT', sequelize.col('id')), 'post_count']
    ],
    group: ['userId'],
    having: sequelize.where(
      sequelize.fn('COUNT', sequelize.col('id')),
      { [Op.gt]: 5 }
    )
  });
}

8. Transactions

Transactions ensure that a series of database operations either all succeed or all fail. This is critical for maintaining data integrity in financial systems, inventory management, and any multi-step operation.

const sequelize = require('./config/database');
const User = require('./models/user');
const Account = require('./models/account');

async function transferFunds(fromId, toId, amount) {
  const transaction = await sequelize.transaction();
  
  try {
    const fromAccount = await Account.findByPk(fromId, { transaction });
    const toAccount = await Account.findByPk(toId, { transaction });
    
    if (fromAccount.balance < amount) {
      throw new Error('Insufficient funds');
    }
    
    await fromAccount.update(
      { balance: fromAccount.balance - amount },
      { transaction }
    );
    
    await toAccount.update(
      { balance: toAccount.balance + amount },
      { transaction }
    );
    
    await transaction.commit();
    console.log('Transfer completed successfully');
  } catch (error) {
    await transaction.rollback();
    console.error('Transfer failed, rolled back:', error.message);
  }
}

For simpler cases, you can use managed transactions that automatically commit or roll back:

async function managedTransaction() {
  try {
    const result = await sequelize.transaction(async (t) => {
      const user = await User.create(
        { firstName: 'John', lastName: 'Smith', email: 'john@example.com' },
        { transaction: t }
      );
      
      await Account.create(
        { userId: user.id, balance: 100 },
        { transaction: t }
      );
      
      return user;
    });
    // Transaction is automatically committed
    console.log('User and account created:', result.toJSON());
  } catch (error) {
    // Transaction is automatically rolled back
    console.error('Operation failed:', error);
  }
}

9. Hooks (Lifecycle Events)

Hooks (also called lifecycle events) are functions that run before or after specific model operations. They are useful for hashing passwords, normalizing data, logging, and enforcing business rules.

const bcrypt = require('bcrypt');

const User = sequelize.define('User', {
  email: { type: DataTypes.STRING, allowNull: false, unique: true },
  password: { type: DataTypes.STRING, allowNull: false }
}, {
  hooks: {
    beforeCreate: async (user) => {
      if (user.password) {
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(user.password, salt);
      }
      user.email = user.email.toLowerCase().trim();
    },
    beforeUpdate: async (user) => {
      if (user.changed('password')) {
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(user.password, salt);
      }
    },
    afterCreate: (user) => {
      console.log(`New user registered: ${user.email}`);
      // Send welcome email, trigger analytics, etc.
    },
    afterDestroy: (user) => {
      console.log(`User deleted: ${user.email}`);
    }
  }
});

10. Raw Queries

Sometimes you need the full power of SQL. Sequelize allows you to run raw queries while still benefiting from connection pooling and parameter binding.

async function rawQueryExample() {
  // Simple raw query
  const [results, metadata] = await sequelize.query(
    "SELECT * FROM users WHERE age > :minAge",
    {
      replacements: { minAge: 18 },
      type: sequelize.QueryTypes.SELECT
    }
  );
  
  // Raw query with model binding
  const users = await sequelize.query(
    "SELECT * FROM users WHERE email = :email",
    {
      replacements: { email: 'jane@example.com' },
      model: User,
      mapToModel: true
    }
  );
  
  return users;
}

11. Seeders

Seeders populate your database with default or test data. They are especially useful for development and testing environments.

npx sequelize-cli seed:generate --name demo-users
'use strict';

module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.bulkInsert('users', [
      {
        first_name: 'Admin',
        last_name: 'User',
        email: 'admin@example.com',
        age: 30,
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        first_name: 'Test',
        last_name: 'User',
        email: 'test@example.com',
        age: 25,
        created_at: new Date(),
        updated_at: new Date()
      }
    ], {});
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.bulkDelete('users', null, {});
  }
};

Run seeders with:

npx sequelize-cli db:seed:all
npx sequelize-cli db:seed:undo

12. Best Practices

Conclusion

Sequelize is a powerful and feature-rich ORM that can take you from simple CRUD applications to complex, transactional systems with rich data relationships. By mastering models, migrations, associations, advanced querying, transactions, and hooks, you gain a complete toolkit for managing relational data in Node.js. The key to becoming an expert is not just knowing the API, but understanding when to use each feature, how to structure your data layer for maintainability, and how to avoid common pitfalls like N+1 queries and unmanaged schema changes. Start with the basics, build a small project, and progressively incorporate advanced features as your application's complexity grows. With consistent practice and adherence to best practices, Sequelize will become an indispensable part of your backend development workflow.

— Ad —

Google AdSense will appear here after approval

← Back to all articles