Database Integration for Full-Stack Apps
Express + MySQL Integration Patterns
Course lectures and practices for JavaScript full‑stack web development with AI‑assisted workflows.
Database Integration 🗄️
2-Hour Focus: Express + MySQL → Production Patterns
🎯 Today’s Mission
Hour 1: MySQL + Express Integration (connection pools, query patterns, error handling)
Hour 2: Advanced Patterns (transactions, migrations, performance optimization)
Using: Review Aggregator backend from Session 4
Goal: Replace in-memory arrays with persistent MySQL storage! 🎉
Prerequisites: You know SQL basics (SELECT, INSERT, UPDATE, DELETE, JOINs)
Focus: Integration patterns, not SQL syntax
🗺️ The Integration Journey
Hour 1: MYSQL INTEGRATION Hour 2: PRODUCTION PATTERNS
┌─────────────────┐ ┌─────────────────┐
│ Connection Pool │ │ Transactions │
│ Query Patterns │ → │ Migrations │
│ Error Handling │ │ Optimization │
│ Replace Arrays │ │ Security │
└─────────────────┘ └─────────────────┘
Working Database Production Ready
Hour 1: MySQL + Express Integration
0:00 - 1:00
💡 Why Database Integration Matters
The Problem with In-Memory Storage (from Session 4):
// Session 4: In-memory arrays
let reviews = [
id: 1, title: "Great product!", rating: 5 }
{ ;
]
// Problems:
❌ Data lost on server restart
❌ No concurrent access protection
❌ Memory limitations
❌ No data relationships/recovery ❌ No backup
Database Integration Benefits:
// With MySQL
const [rows] = await db.execute(
'SELECT * FROM reviews WHERE product_id = ?',
[productId];
)
// Benefits:
✅ Data persists across restarts
✅ ACID transactions
✅ Concurrent access safeof records
✅ Scalable to millions
✅ Automatic backups ✅ Rich query capabilities
🔧 Connection Pool Setup
Why Connection Pools?
// ❌ Bad: Create new connection each time
async function getReviews() {
const connection = mysql.createConnection(config);
const result = await connection.query('SELECT * FROM reviews');
.end(); // Expensive!
connectionreturn result;
}
// ✅ Good: Use connection pool
const pool = mysql.createPool(config);
async function getReviews() {
const [rows] = await pool.execute('SELECT * FROM reviews');
return rows; // Pool manages connections
}
Pool Configuration:
// config/database.js
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'review_aggregator',
waitForConnections: true,
connectionLimit: 10, // Max simultaneous connections
queueLimit: 0, // No limit on queued requests
acquireTimeout: 60000, // 60s timeout to get connection
timeout: 60000, // 60s query timeout
reconnect: true // Auto-reconnect on disconnect
;
})
.exports = pool; module
📊 Database Schema Design
Review Aggregator Schema:
-- Products table (reference)
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
VARCHAR(255) NOT NULL,
name
description TEXT,DECIMAL(10,2),
price TIMESTAMP DEFAULT CURRENT_TIMESTAMP
created_at
);
-- Reviews table (our focus)
CREATE TABLE reviews (
id INT AUTO_INCREMENT PRIMARY KEY,
INT NOT NULL,
product_id source ENUM('Amazon', 'BestBuy', 'Walmart') NOT NULL,
VARCHAR(100) NOT NULL,
external_review_id VARCHAR(255),
author INT CHECK (rating BETWEEN 1 AND 5),
rating VARCHAR(500),
title
content TEXT,
review_date DATETIME,TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
fetched_at
-- Prevent duplicates
UNIQUE KEY unique_review (product_id, source, external_review_id),
-- Performance indexes
INDEX idx_product_id (product_id),
INDEX idx_source (source),
INDEX idx_rating (rating),
-- Foreign key
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);
Key Design Decisions: - UNIQUE constraint prevents duplicate reviews - Indexes on commonly filtered columns - ENUM for source (controlled values) - Foreign key maintains referential integrity
🔄 Query Patterns
Replace GET /api/reviews with SQL
Before (Session 4):
.get('/api/reviews', (req, res) => {
appconst { product_id, source } = req.query;
let filtered = reviews.filter(r => r.product_id === product_id);
if (source) filtered = filtered.filter(r => r.source === source);
.json(filtered);
res; })
After (with MySQL):
const db = require('../config/database');
.get('/api/reviews', async (req, res) => {
apptry {
const { product_id, source, limit = 20, offset = 0 } = req.query;
// Build dynamic query
let sql = 'SELECT * FROM reviews WHERE product_id = ?';
let params = [product_id];
if (source) {
+= ' AND source = ?';
sql .push(source);
params
}
+= ' ORDER BY review_date DESC LIMIT ? OFFSET ?';
sql .push(parseInt(limit), parseInt(offset));
params
const [rows] = await db.execute(sql, params);
.json({
ressuccess: true,
data: rows,
meta: { count: rows.length, limit, offset }
;
})catch (error) {
} console.error('Database error:', error);
.status(500).json({
ressuccess: false,
error: 'Failed to fetch reviews'
;
})
}; })
Replace POST /api/reviews with SQL
Pattern: INSERT with Duplicate Handling
.post('/api/reviews', async (req, res) => {
apptry {
const {
,
product_id,
source,
external_review_id,
author,
rating,
title,
content
review_date= req.body;
}
const [result] = await db.execute(
`INSERT INTO reviews
(product_id, source, external_review_id, author, rating, title, content, review_date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
, source, external_review_id, author, rating, title, content, review_date]
[product_id;
)
// Fetch the created review
const [rows] = await db.execute(
'SELECT * FROM reviews WHERE id = ?',
.insertId]
[result;
)
.status(201).json({
ressuccess: true,
data: rows[0]
;
})
catch (error) {
} // Handle MySQL errors
if (error.code === 'ER_DUP_ENTRY') {
return res.status(409).json({
success: false,
error: 'Review already exists',
code: 'DUPLICATE_REVIEW'
;
})
}
if (error.code === 'ER_NO_REFERENCED_ROW_2') {
return res.status(400).json({
success: false,
error: 'Product not found',
code: 'INVALID_PRODUCT'
;
})
}
console.error('Database error:', error);
.status(500).json({
ressuccess: false,
error: 'Failed to create review'
;
})
}; })
Aggregate Statistics with SQL
Calculate stats directly in database:
.get('/api/reviews/aggregate', async (req, res) => {
apptry {
const { product_id } = req.query;
// Overall statistics
const [overall] = await db.execute(
`SELECT
AVG(rating) as average_rating,
COUNT(*) as total_reviews,
MIN(rating) as min_rating,
MAX(rating) as max_rating
FROM reviews
WHERE product_id = ?`,
[product_id];
)
// Per-source breakdown
const [sources] = await db.execute(
`SELECT
source,
AVG(rating) as average_rating,
COUNT(*) as review_count
FROM reviews
WHERE product_id = ?
GROUP BY source
ORDER BY review_count DESC`,
[product_id];
)
// Rating distribution
const [distribution] = await db.execute(
`SELECT
rating,
COUNT(*) as count
FROM reviews
WHERE product_id = ?
GROUP BY rating
ORDER BY rating DESC`,
[product_id];
)
// Transform distribution to object
const ratingHistogram = { "5": 0, "4": 0, "3": 0, "2": 0, "1": 0 };
.forEach(row => {
distribution.rating.toString()] = row.count;
ratingHistogram[row;
})
.json({
ressuccess: true,
data: {
overall: {
average_rating: overall[0].average_rating
? Math.round(overall[0].average_rating * 10) / 10
: 0,
total_reviews: overall[0].total_reviews,
min_rating: overall[0].min_rating,
max_rating: overall[0].max_rating
,
}by_source: sources.map(row => ({
source: row.source,
average_rating: Math.round(row.average_rating * 10) / 10,
review_count: row.review_count
,
}))rating_histogram: ratingHistogram
};
})
catch (error) {
} console.error('Aggregate query error:', error);
.status(500).json({
ressuccess: false,
error: 'Failed to calculate statistics'
;
})
}; })
🔒 SQL Injection Prevention
Always Use Parameterized Queries:
// ❌ DANGEROUS - SQL Injection vulnerability
.get('/api/reviews/:id', async (req, res) => {
appconst sql = `SELECT * FROM reviews WHERE id = ${req.params.id}`;
const [rows] = await db.execute(sql); // NEVER DO THIS!
;
})
// Attacker sends: /api/reviews/1; DROP TABLE reviews; --
// Your database is gone! 💥
// ✅ SAFE - Parameterized query
.get('/api/reviews/:id', async (req, res) => {
appconst [rows] = await db.execute(
'SELECT * FROM reviews WHERE id = ?',
.params.id]
[req;
)// MySQL automatically escapes the parameter
; })
Dynamic WHERE Clauses (Safe Way):
function buildWhereClause(filters) {
const conditions = [];
const params = [];
if (filters.product_id) {
.push('product_id = ?');
conditions.push(filters.product_id);
params
}
if (filters.source) {
.push('source = ?');
conditions.push(filters.source);
params
}
if (filters.min_rating) {
.push('rating >= ?');
conditions.push(filters.min_rating);
params
}
const whereClause = conditions.length > 0
? 'WHERE ' + conditions.join(' AND ')
: '';
return { whereClause, params };
}
// Usage
const filters = req.query;
const { whereClause, params } = buildWhereClause(filters);
const sql = `SELECT * FROM reviews ${whereClause} ORDER BY review_date DESC`;
const [rows] = await db.execute(sql, params);
You should now have: - ✅ Connection pool setup and tested - ✅ Replaced GET endpoints with SQL queries - ✅ Implemented POST with duplicate handling - ✅ Added aggregate statistics calculation - ✅ Proper SQL injection prevention
Hour 2 Preview: Transactions, migrations, performance optimization!
Hour 2: Production Patterns
1:00 - 2:00
🔄 Database Transactions
Why Transactions Matter:
// Scenario: Fetch reviews AND update product stats
// WITHOUT transaction - can fail partially:
async function fetchAndUpdateStats(productId) {
// Step 1: Insert new reviews (succeeds)
await insertReviews(newReviews);
// Step 2: Update product stats (fails - server crash!)
await updateProductStats(productId);
// Result: New reviews exist but stats are stale!
// Data inconsistency! 😱
}
WITH Transaction - All or Nothing:
async function fetchAndUpdateStats(productId) {
const connection = await db.getConnection();
try {
await connection.beginTransaction();
// Step 1: Insert reviews
for (const review of newReviews) {
await connection.execute(
'INSERT INTO reviews (...) VALUES (...)',
.data]
[review;
)
}
// Step 2: Update product average
await connection.execute(
'UPDATE products SET avg_rating = (SELECT AVG(rating) FROM reviews WHERE product_id = ?) WHERE id = ?',
, productId]
[productId;
)
await connection.commit();
console.log('✅ All operations completed successfully');
catch (error) {
} await connection.rollback();
console.error('❌ Transaction failed, rolling back:', error);
throw error;
finally {
} .release();
connection
} }
📋 Database Migrations
What are Migrations?
Scripts that version your database schema like Git versions your code.
Migration Structure:
migrations/
├── 001_create_products_table.sql
├── 002_create_reviews_table.sql
├── 003_add_review_indexes.sql
├── 004_add_sentiment_column.sql
└── migrate.js
Example Migration:
-- migrations/003_add_review_indexes.sql
-- Description: Add performance indexes for common queries
-- Date: 2025-10-02
-- Add indexes
CREATE INDEX idx_reviews_product_source ON reviews(product_id, source);
CREATE INDEX idx_reviews_rating_date ON reviews(rating, review_date);
-- Rollback (if needed):
-- DROP INDEX idx_reviews_product_source ON reviews;
-- DROP INDEX idx_reviews_rating_date ON reviews;
Migration Runner:
// scripts/migrate.js
const fs = require('fs');
const path = require('path');
const db = require('../config/database');
async function runMigrations() {
try {
// Create migrations table if not exists
await db.execute(`
CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Get executed migrations
const [executed] = await db.execute('SELECT filename FROM migrations');
const executedSet = new Set(executed.map(row => row.filename));
// Get all migration files
const migrationsDir = path.join(__dirname, '../migrations');
const files = fs.readdirSync(migrationsDir)
.filter(file => file.endsWith('.sql'))
.sort();
// Run pending migrations
for (const file of files) {
if (executedSet.has(file)) {
console.log(`⏭️ Skipping ${file} (already executed)`);
continue;
}
console.log(`🔄 Executing ${file}...`);
const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
await db.execute(sql);
// Record as executed
await db.execute(
'INSERT INTO migrations (filename) VALUES (?)',
[file];
)
console.log(`✅ Completed ${file}`);
}
console.log('🎉 All migrations completed!');
catch (error) {
} console.error('❌ Migration failed:', error);
process.exit(1);
}
}
runMigrations();
Usage:
npm run migrate # Run pending migrations
⚡ Performance Optimization
Query Optimization
Use EXPLAIN to Analyze Queries:
-- Check if your query uses indexes
EXPLAIN SELECT * FROM reviews
WHERE product_id = 1 AND source = 'Amazon'
ORDER BY review_date DESC
LIMIT 10;
-- Look for:
-- ✅ type: 'ref' or 'range' (good)
-- ❌ type: 'ALL' (full table scan - bad!)
-- ✅ Extra: 'Using index' (great!)
-- ❌ Extra: 'Using filesort' (slow!)
Add Strategic Indexes:
-- For common query patterns
CREATE INDEX idx_product_source_date ON reviews(product_id, source, review_date);
CREATE INDEX idx_rating_filter ON reviews(product_id, rating);
Connection Pool Tuning
Monitor Pool Usage:
// Add pool monitoring
setInterval(() => {
// Use an instrumentation helper exported from your database config
// (see config/database.js for a lightweight wrapper example)
const poolStats = getPoolStats();
console.log('Pool status:', poolStats);
, 30000); // Every 30 seconds }
Optimize Pool Size:
const pool = mysql.createPool({
// ...other config
connectionLimit: process.env.NODE_ENV === 'production' ? 20 : 5,
acquireTimeout: 30000, // Shorter timeout
timeout: 30000, // Query timeout
idleTimeout: 300000, // 5 minutes idle timeout
reconnect: true,
multipleStatements: false // Security: prevent multiple statements
; })
📊 Monitoring & Logging
Query Performance Logging:
// middleware/queryLogger.js
function logSlowQueries(threshold = 1000) {
const originalExecute = db.execute;
.execute = async function(sql, params) {
dbconst start = Date.now();
try {
const result = await originalExecute.call(this, sql, params);
const duration = Date.now() - start;
if (duration > threshold) {
console.warn(`🐌 Slow query (${duration}ms):`, {
sql: sql.substring(0, 100) + '...',
params: params?.slice(0, 5),
duration;
})
}
return result;
catch (error) {
} const duration = Date.now() - start;
console.error(`❌ Query failed (${duration}ms):`, {
sql: sql.substring(0, 100) + '...',
params: params?.slice(0, 5),
error: error.message
;
})throw error;
};
}
}
// Enable in development
if (process.env.NODE_ENV === 'development') {
logSlowQueries(500); // Log queries > 500ms
}
Health Monitoring:
// routes/health.js
.get('/health/detailed', async (req, res) => {
appconst health = {
timestamp: new Date().toISOString(),
service: 'review-aggregator-api',
version: process.env.npm_package_version,
environment: process.env.NODE_ENV,
uptime: process.uptime(),
memory: process.memoryUsage(),
database: { status: 'unknown' }
;
}
try {
const start = Date.now();
const [rows] = await db.execute('SELECT COUNT(*) as review_count FROM reviews');
const queryTime = Date.now() - start;
// Use a safe helper to expose pool stats if available. Avoid accessing
// internal pool properties (like db.pool.totalConnections) directly as
// these internals are not portable across mysql2 versions.
let poolStatus = { connectionLimit: null, activeConnections: null };
try {
if (typeof db.getPoolStats === 'function') {
= db.getPoolStats();
poolStatus else if (db.pool && db.pool.config && db.pool.config.connectionLimit) {
} .connectionLimit = db.pool.config.connectionLimit;
poolStatus
}catch (e) {
} // best-effort only
}
.database = {
healthstatus: 'healthy',
response_time_ms: queryTime,
total_reviews: rows[0].review_count,
pool_status: poolStatus
;
}
const overallStatus = queryTime < 1000 ? 'healthy' : 'degraded';
.status(overallStatus === 'healthy' ? 200 : 503).json({
resstatus: overallStatus,
...health
;
})
catch (error) {
} .database = {
healthstatus: 'unhealthy',
error: error.message
;
}
.status(503).json({
resstatus: 'unhealthy',
...health
;
})
}; })
🔐 Security Best Practices
Database User Permissions:
-- Create app-specific database user (not root!)
CREATE USER 'review_app'@'localhost' IDENTIFIED BY 'strong_password_here';
-- Grant only necessary permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON review_aggregator.* TO 'review_app'@'localhost';
-- No DDL permissions (CREATE, DROP, ALTER) for app user
-- Use separate admin user for migrations
Environment Variables:
# .env
DB_HOST=localhost
DB_USER=review_app
DB_PASSWORD=strong_password_here
DB_NAME=review_aggregator
DB_PORT=3306
# Production: Use secrets management
# AWS: Parameter Store, Secrets Manager
# Azure: Key Vault
# Google: Secret Manager
Input Validation Middleware:
const { body, query, validationResult } = require('express-validator');
const validateReviewInput = [
body('product_id').isString().isLength({ min: 1, max: 50 }),
body('source').isIn(['Amazon', 'BestBuy', 'Walmart']),
body('rating').isInt({ min: 1, max: 5 }),
body('title').optional().isString().isLength({ max: 500 }),
body('content').optional().isString().isLength({ max: 5000 }),
, res, next) => {
(reqconst errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array()
;
})
}next();
};
]
.post('/api/reviews', validateReviewInput, async (req, res) => {
app// Input is validated and sanitized
; })
🎯 Complete Integration Example
Full Review Fetch Endpoint (Production-Ready):
.post('/api/products/:id/fetch',
app,
validateProductId,
rateLimitByUserasync (req, res) => {
const productId = req.params.id;
const connection = await db.getConnection();
try {
await connection.beginTransaction();
// 1. Verify product exists
const [products] = await connection.execute(
'SELECT id FROM products WHERE id = ?',
[productId];
)
if (products.length === 0) {
return res.status(404).json({
success: false,
error: 'Product not found'
;
})
}
// 2. Call external scraper
const scraperResponse = await fetch(`${SCRAPER_URL}/scrape/${productId}`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${SCRAPER_API_KEY}` }
;
})
if (!scraperResponse.ok) {
throw new Error('Scraper service unavailable');
}
const newReviews = await scraperResponse.json();
// 3. Insert new reviews (skip duplicates)
let insertCount = 0;
for (const review of newReviews) {
try {
await connection.execute(
`INSERT INTO reviews
(product_id, source, external_review_id, author, rating, title, content, review_date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
, review.source, review.id, review.author, review.rating, review.title, review.body, review.date]
[productId;
)++;
insertCountcatch (error) {
} if (error.code === 'ER_DUP_ENTRY') {
// Skip duplicate, continue with others
continue;
}throw error; // Re-throw other errors
}
}
// 4. Update product statistics
await connection.execute(
`UPDATE products SET
avg_rating = (SELECT AVG(rating) FROM reviews WHERE product_id = ?),
review_count = (SELECT COUNT(*) FROM reviews WHERE product_id = ?),
last_scraped = NOW()
WHERE id = ?`,
, productId, productId]
[productId;
)
await connection.commit();
.json({
ressuccess: true,
message: `Successfully fetched ${insertCount} new reviews`,
data: { new_reviews: insertCount, duplicates_skipped: newReviews.length - insertCount }
;
})
catch (error) {
} await connection.rollback();
console.error('Fetch reviews failed:', error);
if (error.message.includes('Scraper service')) {
return res.status(503).json({
success: false,
error: 'External service temporarily unavailable',
code: 'SCRAPER_UNAVAILABLE'
;
})
}
.status(500).json({
ressuccess: false,
error: 'Failed to fetch reviews',
code: 'FETCH_FAILED'
;
})finally {
} .release();
connection
}
}; )
🎉 Session Summary
What You’ve Learned Today
Hour 1: Integration Basics - ✅ Connection pooling setup - ✅ Parameterized queries (SQL injection prevention) - ✅ Error handling patterns - ✅ CRUD operations with MySQL - ✅ Aggregate statistics - ✅ Health monitoring
Hour 2: Production Patterns - ✅ Database transactions - ✅ Migration systems - ✅ Performance optimization - ✅ Query monitoring - ✅ Security best practices - ✅ Complete integration example
Key Takeaways
- Connection pools are essential for performance and reliability
- Always use parameterized queries to prevent SQL injection
- Transactions ensure data consistency for multi-step operations
- Migrations version your database like Git versions code
- Monitor query performance and add indexes strategically
- Security is layered - database users, input validation, environment variables
From In-Memory to Production Database
You’ve now completed the journey: - ✅ Session 4: Express API with in-memory arrays - ✅ Today: Production database integration - 🔜 Next: Connect React frontend to database-backed API
Your Review Aggregator now has persistent, scalable, secure data storage! 🚀
📚 Practice Session Preview
Immediately after this lecture: 1-hour hands-on practice replacing your in-memory arrays with MySQL database persistence using the patterns learned today.
What you’ll do: - Set up connection pools and database schema - Replace all CRUD operations with SQL queries - Implement transaction-based batch operations - Add comprehensive error handling and monitoring
This prepares you for: Next session’s full-stack integration where your React frontend will consume real, persistent data!
🎯 You’re Now a Database Integration Expert!
Key Achievements: - ⚡ Set up production-ready database connections - 🗄️ Implemented persistent data storage with MySQL - 🔒 Secured operations against SQL injection - 🔄 Added transaction support for data consistency - 📊 Created monitoring and health checks - 🏗️ Built maintainable, scalable code architecture
Next Steps: Practice session to implement these patterns, then connect to your React frontend! 🚀