DEV Community

Md. Maruf Rahman
Md. Maruf Rahman

Posted on

Sequelize ORM with MySQL Setup: Complete Guide to Production-Ready Database Management

If you've ever found yourself wrestling with raw SQL queries in Node.js or struggling to manage database connections efficiently, you're not alone. I've been there too, and that's exactly why I want to share this comprehensive guide on setting up Sequelize ORM with MySQL.

Sequelize is one of the most popular ORM (Object-Relational Mapping) libraries for Node.js, and for good reason. It provides a clean, intuitive interface for database operations while handling all the complex SQL under the hood. Whether you're building an inventory management system, an e-commerce platform, or any data-driven application, Sequelize can make your life significantly easier.

In this guide, I'll walk you through setting up Sequelize with MySQL using a singleton pattern for connection management—a production-ready approach that I've used in multiple real-world projects. By the end, you'll have a solid foundation that's both scalable and maintainable.

💡 Full Article: For the complete version with additional code examples, troubleshooting tips, and more advanced patterns, check out the full post on my website: Sequelize ORM with MySQL Setup: Complete Guide


Why Sequelize?

Before we dive into the code, let me share why Sequelize has become my go-to choice for Node.js database operations:

  • Clean API: Write database queries using JavaScript instead of raw SQL
  • Type Safety: Better code completion and fewer runtime errors
  • Migrations: Version control for your database schema
  • Associations: Handle relationships between tables effortlessly
  • Connection Pooling: Built-in support for efficient connection management
  • Cross-Database Support: Works with MySQL, PostgreSQL, SQLite, and more

Now, let's get our hands dirty!


Installation

First things first, let's install the necessary packages. You'll need Sequelize itself, the MySQL2 driver (which is the recommended MySQL driver for Sequelize), and dotenv for managing environment variables.

npm install sequelize mysql2
npm install dotenv
Enter fullscreen mode Exit fullscreen mode

That's it! You're ready to start building.


Database Connection with Singleton Pattern

One of the most important decisions you'll make when setting up Sequelize is how to manage your database connection. I've found that using a singleton pattern is the best approach for most applications. It ensures you only have one database connection instance throughout your application lifecycle, which prevents connection leaks and improves performance.

Here's a production-ready database connection class I've used in multiple projects:

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

class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }

    this.sequelize = new Sequelize(
      process.env.DB_NAME || "inventory_management",
      process.env.DB_USER || "root",
      process.env.DB_PASSWORD || "",
      {
        host: process.env.DB_HOST || "localhost",
        port: process.env.DB_PORT || 3306,
        dialect: "mysql",

        // Connection Pool Configuration
        pool: {
          max: 10,              // Maximum connections
          min: 0,               // Minimum connections
          acquire: 30000,       // Max time to get connection
          idle: 10000,          // Max time connection can be idle
        },

        // Logging
        logging: process.env.NODE_ENV === "production" ? false : console.log,

        // Retry Configuration
        retry: {
          max: 3,
          match: [
            Sequelize.ConnectionError,
            Sequelize.ConnectionTimedOutError,
          ],
        },

        // Query timeout
        dialectOptions: {
          connectTimeout: 60000,
          decimalNumbers: true,
        },

        // Timezone
        timezone: "+00:00",

        // Model defaults
        define: {
          timestamps: true,
          underscored: false,
          freezeTableName: true,
          charset: "utf8mb4",
          collate: "utf8mb4_unicode_ci",
        },
      }
    );

    Database.instance = this;
  }

  async testConnection() {
    try {
      await this.sequelize.authenticate();
      console.log("✅ MySQL Database connected successfully");
      return true;
    } catch (error) {
      console.error("❌ Database connection failed:", error.message);
      return false;
    }
  }

  async closeConnection() {
    try {
      await this.sequelize.close();
      console.log("✅ Database connection closed");
      return true;
    } catch (error) {
      console.error("❌ Error closing connection:", error.message);
      return false;
    }
  }
}

const database = new Database();
module.exports = database;
Enter fullscreen mode Exit fullscreen mode

Key Features of This Setup

Singleton Pattern: The if (Database.instance) check ensures that no matter how many times you import this module, you'll always get the same instance. This is crucial for preventing multiple database connections.

Connection Pooling: The pool configuration manages a pool of database connections. This is essential for production applications as it:

  • Reuses connections instead of creating new ones for each query
  • Limits the maximum number of connections to prevent overwhelming your database
  • Handles connection timeouts gracefully

Retry Logic: Network issues happen. The retry configuration automatically retries failed connections up to 3 times, which can save you from temporary network hiccups.

Production vs Development Logging: In production, you typically don't want SQL queries logged to the console (it's noisy and can expose sensitive data). The logging configuration handles this automatically.

UTF8MB4 Support: The charset and collate settings ensure proper support for emojis and international characters—something that's become essential in modern applications.


Creating Models

Now that we have our database connection set up, let's create some models. Models in Sequelize represent tables in your database and define the structure of your data.

Let me show you how to create a Product model with associations to a Category model:

const { DataTypes } = require("sequelize");
const database = require("./database");
const { Category } = require("./index");

const Product = database.sequelize.define("Product", {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true,
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  categoryId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: {
      model: Category,
      key: "id",
    },
  },
  sku: {
    type: DataTypes.STRING,
    allowNull: true,
  },
  description: {
    type: DataTypes.TEXT,
    allowNull: true,
  },
  price: {
    type: DataTypes.DECIMAL(10, 2),
    allowNull: false,
    defaultValue: 0,
  },
  cost: {
    type: DataTypes.DECIMAL(10, 2),
    allowNull: false,
    defaultValue: 0,
  },
  stock: {
    type: DataTypes.INTEGER,
    allowNull: false,
    defaultValue: 0,
  },
  minStock: {
    type: DataTypes.INTEGER,
    allowNull: false,
    defaultValue: 0,
  },
  unit: {
    type: DataTypes.STRING,
    allowNull: false,
    defaultValue: "pcs",
  },
  barcode: {
    type: DataTypes.STRING,
    allowNull: true,
  },
}, {
  tableName: "products",
  timestamps: true,
});

// Associations
Product.belongsTo(Category, { foreignKey: "categoryId", as: "category" });
Category.hasMany(Product, { foreignKey: "categoryId", as: "products" });

module.exports = Product;
Enter fullscreen mode Exit fullscreen mode

Understanding the Model Definition

  • DataTypes: Sequelize provides various data types that map to MySQL types. DECIMAL(10, 2) is perfect for currency values, ensuring precision.
  • allowNull: This validation ensures data integrity at the ORM level.
  • defaultValue: Provides sensible defaults, reducing the need for conditional logic in your application code.
  • Associations: The belongsTo and hasMany relationships define how your models relate to each other. This enables powerful querying features like eager loading.

Model Class for Clean Interface

While you can use Sequelize models directly in your routes or controllers, I've found that wrapping them in a model class provides a cleaner, more maintainable interface. This approach also makes it easier to add business logic and handle errors consistently.

Here's a ProductModel class that wraps all the Sequelize operations:

const { Product, Category } = require("./index");

class ProductModel {
  async getAll() {
    try {
      const products = await Product.findAll({
        include: [{
          model: Category,
          as: "category",
          attributes: ["id", "name"],
        }],
        order: [["createdAt", "DESC"]],
      });

      return products.map(product => {
        const data = product.toJSON();
        return {
          ...data,
          categoryName: data.category ? data.category.name : "Unknown",
        };
      });
    } catch (error) {
      console.error("Error getting all products:", error);
      return [];
    }
  }

  async getById(id) {
    try {
      const product = await Product.findByPk(id, {
        include: [{
          model: Category,
          as: "category",
          attributes: ["id", "name"],
        }],
      });

      if (!product) {
        return { success: false, message: "Product not found" };
      }

      const data = product.toJSON();
      return {
        ...data,
        categoryName: data.category ? data.category.name : "Unknown",
      };
    } catch (error) {
      console.error("Error getting product by ID:", error);
      return { success: false, message: "Error reading product" };
    }
  }

  async create(productData) {
    try {
      return await Product.create(productData);
    } catch (error) {
      console.error("Error creating product:", error);
      return { success: false, message: "Error creating product", error: error.message };
    }
  }

  async update(id, productData) {
    try {
      const product = await Product.findByPk(id);
      if (!product) {
        return { success: false, message: "Product not found" };
      }
      await product.update(productData);
      return product;
    } catch (error) {
      console.error("Error updating product:", error);
      return { success: false, message: "Error updating product" };
    }
  }

  async delete(id) {
    try {
      const product = await Product.findByPk(id);
      if (!product) {
        return { success: false, message: "Product not found" };
      }
      const productData = product.toJSON();
      await product.destroy();
      return productData;
    } catch (error) {
      console.error("Error deleting product:", error);
      return { success: false, message: "Error deleting product" };
    }
  }
}

module.exports = new ProductModel();
Enter fullscreen mode Exit fullscreen mode

Benefits of This Approach

  • Consistent Error Handling: All methods handle errors in the same way
  • Eager Loading: The include option automatically loads related data (Category) in a single query, avoiding N+1 query problems
  • Data Transformation: You can transform the data before returning it (like adding categoryName)
  • Separation of Concerns: Your routes/controllers don't need to know about Sequelize specifics

Environment Variables

Never hardcode your database credentials! Always use environment variables. Create a .env file in your project root:

DB_HOST=localhost
DB_PORT=3306
DB_NAME=inventory_management
DB_USER=root
DB_PASSWORD=your_mysql_password
NODE_ENV=development
Enter fullscreen mode Exit fullscreen mode

Important: Make sure to add .env to your .gitignore file to prevent accidentally committing sensitive credentials to version control.


Testing the Connection

Before you start building your application, it's crucial to test your database connection. Here's how you can do it:

const database = require("./config/database");

async function startServer() {
  const dbConnected = await database.testConnection();

  if (!dbConnected) {
    console.error("Failed to connect to database");
    process.exit(1);
  }

  // Start your Express server
  app.listen(3000, () => {
    console.log("Server running on port 3000");
  });
}

startServer();
Enter fullscreen mode Exit fullscreen mode

This ensures your application won't start if it can't connect to the database, which is much better than discovering connection issues when users try to use your app.


Best Practices I've Learned the Hard Way

After working with Sequelize in production for several years, here are the practices that have saved me countless hours of debugging:

  1. Use Singleton Pattern: Ensures only one database connection instance, preventing connection leaks and improving performance.

  2. Configure Connection Pooling: This is non-negotiable for production. Without it, you'll quickly exhaust your database connection limit.

  3. Always Use Environment Variables: Never commit database credentials. Use .env files and make sure they're in .gitignore.

  4. Implement Proper Error Handling: Database operations can fail for many reasons. Always wrap them in try-catch blocks and provide meaningful error messages.

  5. Use Transactions for Complex Operations: If you're performing multiple related database operations, wrap them in a transaction to ensure data consistency.

  6. Set Appropriate Timeouts: Network issues happen. Configure timeouts that give your queries enough time to complete but don't hang indefinitely.

  7. Close Connections Gracefully: When your application shuts down, close database connections properly. This is especially important in serverless environments.


Conclusion

Sequelize ORM provides a powerful, type-safe interface for MySQL operations in Node.js. Using a singleton pattern ensures efficient connection management, while connection pooling improves performance significantly. This setup is production-ready and scalable for inventory management systems, e-commerce platforms, and other data-heavy applications.

The patterns I've shared here have been battle-tested in real-world applications, and they'll give you a solid foundation to build upon. Remember, good database architecture is the backbone of any successful application.


Want More?

If you found this guide helpful, I've written several other in-depth tutorials on related topics:

You can find the full version of this article with additional code examples, troubleshooting tips, and more advanced patterns on my website: Sequelize ORM with MySQL Setup: Complete Guide

I regularly publish tutorials on Node.js, Express.js, database management, and full-stack development. Check out my blog for more guides and tutorials!


About the Author

I'm Maruf Rahman, a full-stack developer passionate about sharing knowledge and helping fellow developers build better applications. Connect with me on GitHub and LinkedIn!


Originally published on marufrahman.live

Top comments (0)