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