Building a Shopping App with Express and Sequelize

A comprehensive guide to creating a database-driven shopping application

Introduction to Project Architecture

Building a web application is like constructing a building. Just as architects create detailed blueprints before construction begins, we'll plan our database structure and application flow before writing any code. This ensures our application has a solid foundation that can grow and evolve over time.

Initial Project Setup


mkdir shopping_app
cd shopping_app
npm init -y

# Install core dependencies
npm install express express-session sqlite3 sequelize bcrypt cookie-parser dotenv

# Install development dependencies
npm install --save-dev sequelize-cli nodemon
            

Our project structure will look like this:


shopping_app/
  ├── config/
  │   ├── config.json           # Database configuration
  │   └── database.js           # Sequelize instance
  ├── migrations/               # Database structure versions
  ├── seeders/                  # Initial data
  ├── models/                   # Data models
  ├── routes/                   # API endpoints
  ├── middleware/               # Custom middleware
  ├── .env                      # Environment variables
  └── app.js                    # Main application file
            

Setting Up Sequelize CLI and Database Configuration

Think of Sequelize CLI as your database management assistant. It helps you create and maintain your database structure over time, like a librarian keeping track of books and their organization.


# Initialize Sequelize in your project
npx sequelize-cli init
            

Create a .env file for environment variables:


# .env
DB_NAME=shopping_db
DB_USER=root
DB_PASSWORD=
DB_HOST=localhost
SESSION_SECRET=your_secret_key_here
            

Update config/config.json with your database settings:


{
  "development": {
    "dialect": "sqlite",
    "storage": "./database.sqlite"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "./database.test.sqlite"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "./database.production.sqlite"
  }
}
            

Creating Database Migrations

Migrations are like a time machine for your database. They record every structural change, allowing you to move forward or backward in your database's timeline. This is crucial for team collaboration and deployment management.

Users Table Migration


npx sequelize-cli migration:generate --name create-users-table

// migrations/XXXXXXXXXXXXXX-create-users-table.js
'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Users', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      email: {
        type: Sequelize.STRING,
        allowNull: false,
        unique: true
      },
      password: {
        type: Sequelize.STRING,
        allowNull: false
      },
      name: {
        type: Sequelize.STRING,
        allowNull: false
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });

    // Add index for email lookups
    await queryInterface.addIndex('Users', ['email']);
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Users');
  }
};
            

Products Table Migration


npx sequelize-cli migration:generate --name create-products-table

// migrations/XXXXXXXXXXXXXX-create-products-table.js
'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Products', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      name: {
        type: Sequelize.STRING,
        allowNull: false
      },
      description: {
        type: Sequelize.TEXT
      },
      price: {
        type: Sequelize.DECIMAL(10, 2),
        allowNull: false
      },
      stock: {
        type: Sequelize.INTEGER,
        allowNull: false,
        defaultValue: 0
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });

    // Add index for product searches
    await queryInterface.addIndex('Products', ['name']);
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Products');
  }
};
            

Creating Database Seeders

Seeders are like gardeners planting initial seeds in your database. They help you populate your database with test data or essential startup data, making development and testing much easier.

User Seeder


npx sequelize-cli seed:generate --name demo-users

// seeders/XXXXXXXXXXXXX-demo-users.js
'use strict';
const bcrypt = require('bcrypt');

module.exports = {
  up: async (queryInterface, Sequelize) => {
    const hashedPassword = await bcrypt.hash('password123', 10);
    await queryInterface.bulkInsert('Users', [
      {
        name: 'Admin User',
        email: 'admin@example.com',
        password: hashedPassword,
        createdAt: new Date(),
        updatedAt: new Date()
      },
      {
        name: 'Test User',
        email: 'test@example.com',
        password: hashedPassword,
        createdAt: new Date(),
        updatedAt: new Date()
      }
    ]);
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.bulkDelete('Users', null, {});
  }
};
            

Product Seeder


npx sequelize-cli seed:generate --name demo-products

// seeders/XXXXXXXXXXXXX-demo-products.js
'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.bulkInsert('Products', [
      {
        name: 'Laptop Pro',
        description: 'High-performance laptop for professionals',
        price: 1299.99,
        stock: 50,
        createdAt: new Date(),
        updatedAt: new Date()
      },
      {
        name: 'Smartphone X',
        description: 'Latest smartphone with advanced features',
        price: 799.99,
        stock: 100,
        createdAt: new Date(),
        updatedAt: new Date()
      }
    ]);
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.bulkDelete('Products', null, {});
  }
};
            

Setting Up Models

Models are like blueprints that define how our application interacts with the database. They ensure data consistency and provide a clean interface for database operations.

User Model


// models/user.js
const { Model, DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const bcrypt = require('bcrypt');

class User extends Model {
    async validatePassword(password) {
        return bcrypt.compare(password, this.password);
    }
}

User.init({
    email: {
        type: DataTypes.STRING,
        unique: true,
        allowNull: false,
        validate: {
            isEmail: true
        }
    },
    password: {
        type: DataTypes.STRING,
        allowNull: false
    },
    name: {
        type: DataTypes.STRING,
        allowNull: false
    }
}, {
    sequelize,
    modelName: 'User',
    hooks: {
        beforeCreate: async (user) => {
            const salt = await bcrypt.genSalt(10);
            user.password = await bcrypt.hash(user.password, salt);
        }
    }
});

module.exports = User;
            

Implementing Authentication Routes


// routes/auth.js
const router = require('express').Router();
const User = require('../models/user');

router.post('/register', async (req, res) => {
    try {
        const { email, password, name } = req.body;
        const user = await User.create({ email, password, name });
        req.session.userId = user.id;
        res.json({ message: 'Registration successful' });
    } catch (error) {
        res.status(400).json({ error: error.message });
    }
});

router.post('/login', async (req, res) => {
    try {
        const { email, password } = req.body;
        const user = await User.findOne({ where: { email } });
        
        if (!user || !(await user.validatePassword(password))) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }

        req.session.userId = user.id;
        res.json({ message: 'Login successful' });
    } catch (error) {
        res.status(400).json({ error: error.message });
    }
});
            

Setting Up the Main Application


// app.js
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
require('dotenv').config();

const sequelize = require('./config/database');
const authRoutes = require('./routes/auth');

const app = express();

app.use(express.json());
app.use(cookieParser());
app.use(session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false
}));

app.use('/auth', authRoutes);

const PORT = process.env.PORT || 3000;

async function startServer() {
    try {
        // Run migrations
        await sequelize.sync();
        console.log('Database synchronized');
        
        app.listen(PORT, () => {
            console.log(`Server running on port ${PORT}`);
        });
    } catch (error) {
        console.error('Server startup error:', error);
    }
}

startServer();
            

Development Workflow

Add these scripts to your package.json for easier development:


{
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js",
    "db:migrate": "sequelize-cli db:migrate",
    "db:seed": "sequelize-cli db:seed:all",
    "db:reset": "sequelize-cli db:migrate:undo:all && sequelize-cli db:migrate && sequelize-cli db:seed:all"
  }
}
            

Start your development server:

npm run dev

Initialize your database:


npm run db:migrate  # Run migrations
npm run db:seed    # Seed initial data
            

Testing the Application

Test your endpoints using curl or Postman:


# Register a new user
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123","name":"Test User"}'

# Login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
            

Next Steps

With our foundation in place, consider implementing:

- Shopping cart functionality

- Order processing

- User profiles

- Product categories

- Search functionality

- Admin dashboard