Express with SQL Databases

MongoDB suits flexible, document-shaped data, but many real-world applications use SQL databases like MySQL or PostgreSQL. SQL databases store data in structured tables with relationships between them — ideal for banking systems, e-commerce platforms, and any application where data integrity and complex queries matter. This topic connects Express to a SQL database using Sequelize, the most popular SQL ORM for Node.js.

SQL vs NoSQL: The Right Tool for the Job

Think of a SQL database like a spreadsheet with strict rules: every row must fill in every column, you cannot add a column halfway through, and tables can reference each other. A NoSQL database (MongoDB) is like a folder of sticky notes — each note can have different information and you can add fields at any time.

SQL Database (PostgreSQL / MySQL):
┌─────┬──────────┬────────────────────┬─────────┐
│ id  │ name     │ email              │ role    │
├─────┼──────────┼────────────────────┼─────────┤
│  1  │ Alice    │ alice@example.com  │ admin   │
│  2  │ Bob      │ bob@example.com    │ user    │
└─────┴──────────┴────────────────────┴─────────┘
       ↑ Every row must have every column

NoSQL Database (MongoDB):
  { _id: 1, name: "Alice", email: "alice@example.com", role: "admin" }
  { _id: 2, name: "Bob", hobbies: ["reading"] }  ← Different fields OK

When to Choose SQL

┌──────────────────────────────────────────────────────────────────┐
│              Choose SQL When...                                  │
├──────────────────────────────────────────────────────────────────┤
│ Data has fixed structure (user accounts, orders, products)       │
│ Data has clear relationships (users have orders, orders have     │
│   products)                                                      │
│ Data integrity is critical (financial transactions)              │
│ You need complex multi-table queries (JOINs, aggregations)       │
│ Your team already uses MySQL or PostgreSQL                       │
└──────────────────────────────────────────────────────────────────┘

Install Sequelize and a Database Driver

Sequelize supports PostgreSQL, MySQL, MariaDB, SQLite, and MSSQL. Install Sequelize plus the driver for your chosen database:

npm install sequelize

# PostgreSQL:
npm install pg pg-hstore

# MySQL:
npm install mysql2

# SQLite (great for development, no server needed):
npm install sqlite3

Connect to the Database

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

// PostgreSQL connection
const sequelize = new Sequelize(process.env.DATABASE_URL, {
  dialect: 'postgres',
  logging: false // Disable SQL query logging (set to console.log to see queries)
});

// SQLite (file-based, ideal for development)
const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: './database.sqlite'
});

// Test the connection
const connectDB = async () => {
  try {
    await sequelize.authenticate();
    console.log('Database connected successfully');
  } catch (err) {
    console.error('Connection failed:', err);
    process.exit(1);
  }
};

connectDB();

Define a Model

A Sequelize model defines a table's columns, data types, and validation rules — similar to a Mongoose schema for MongoDB:

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

const User = sequelize.define('User', {
  id: {
    type: DataTypes.INTEGER,
    autoIncrement: true,
    primaryKey: true
  },
  name: {
    type: DataTypes.STRING(100),
    allowNull: false,
    validate: {
      notEmpty: true,
      len: [2, 100]
    }
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true
    }
  },
  role: {
    type: DataTypes.ENUM('user', 'admin'),
    defaultValue: 'user'
  }
}, {
  tableName: 'users',   // Explicit table name
  timestamps: true      // Adds createdAt and updatedAt columns automatically
});

module.exports = User;

Sync Models with the Database

Sequelize can create database tables directly from your models:

// Sync all models — creates tables if they don't exist
await sequelize.sync();

// Sync and drop existing tables first (DANGEROUS — only for development)
await sequelize.sync({ force: true });

// Add new columns without dropping existing data
await sequelize.sync({ alter: true });

In production, use database migrations instead of sync() to manage schema changes safely without data loss.

CRUD Operations with Sequelize

CREATE

app.post('/api/users', async (req, res) => {
  try {
    const user = await User.create({
      name: req.body.name,
      email: req.body.email
    });
    res.status(201).json({ success: true, data: user });
  } catch (err) {
    if (err.name === 'SequelizeUniqueConstraintError') {
      return res.status(400).json({ error: 'Email already exists' });
    }
    res.status(400).json({ error: err.message });
  }
});

READ ALL with Filtering

app.get('/api/users', async (req, res) => {
  try {
    const { role, search } = req.query;
    const where = {};

    if (role) where.role = role;
    if (search) {
      const { Op } = require('sequelize');
      where.name = { [Op.like]: `%${search}%` };
    }

    const users = await User.findAll({
      where,
      attributes: ['id', 'name', 'email', 'role', 'createdAt'],
      order: [['createdAt', 'DESC']]
    });

    res.json({ count: users.length, data: users });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

READ ONE

app.get('/api/users/:id', async (req, res) => {
  try {
    const user = await User.findByPk(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json({ data: user });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

UPDATE

app.patch('/api/users/:id', async (req, res) => {
  try {
    const user = await User.findByPk(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });

    await user.update(req.body);
    res.json({ data: user });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

DELETE

app.delete('/api/users/:id', async (req, res) => {
  try {
    const user = await User.findByPk(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });

    await user.destroy();
    res.json({ message: 'User deleted' });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

Table Relationships (Associations)

SQL databases excel at modeling relationships between tables. Sequelize defines these in code:

const Post = sequelize.define('Post', {
  title: { type: DataTypes.STRING, allowNull: false },
  content: { type: DataTypes.TEXT }
});

// One User has many Posts
User.hasMany(Post, { foreignKey: 'userId', as: 'posts' });
Post.belongsTo(User, { foreignKey: 'userId', as: 'author' });

// Sync both models
await sequelize.sync();

Query a user along with all their posts in one call using include:

app.get('/api/users/:id/posts', async (req, res) => {
  const user = await User.findByPk(req.params.id, {
    include: [{ model: Post, as: 'posts' }]
  });

  if (!user) return res.status(404).json({ error: 'User not found' });

  res.json({ data: user });
});

Sequelize generates this SQL behind the scenes:

SELECT users.*, posts.*
FROM users
LEFT JOIN posts ON posts.userId = users.id
WHERE users.id = 1;

Sequelize Operators for Complex Queries

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

// Users created in the last 7 days
const recent = await User.findAll({
  where: {
    createdAt: { [Op.gte]: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
  }
});

// Users with name starting with 'A' or role of 'admin'
const filtered = await User.findAll({
  where: {
    [Op.or]: [
      { name: { [Op.startsWith]: 'A' } },
      { role: 'admin' }
    ]
  }
});

Summary

SQL databases use structured tables with strict schemas and support powerful relationships between data. Sequelize connects Express to SQL databases including PostgreSQL, MySQL, and SQLite. Define models with sequelize.define() to describe table columns, data types, and validations. Use sequelize.sync() in development to create tables from models. Perform CRUD with Model.create(), Model.findAll(), Model.findByPk(), instance.update(), and instance.destroy(). Model associations like hasMany and belongsTo let Sequelize generate SQL JOINs automatically when you use the include option on queries. Use Sequelize's Op operators for flexible filtering without writing raw SQL.

Leave a Comment

Your email address will not be published. Required fields are marked *