Building REST APIs for Full-Stack Applications
2025-10-11
2-Hour Focus: APIs β Database β Production Ready
What Weβre Building (2 Hours)
Hour 1: Express Fundamentals (server, routing, middleware, REST APIs)
Hour 2: Database Integration (MySQL, data persistence, production patterns)
Using: fullstack-minimal-app
backend foundation
Goal: Build a complete API that serves the React frontend from last session! π
Philosophy: Practical backend skills, database-driven applications, production-ready code
Hour 1: EXPRESS FOUNDATIONS Hour 2: DATABASE & PRODUCTION
βββββββββββββββββββ βββββββββββββββββββ
β Why Node/Expressβ β MySQL Setup β
β REST API Design β β β Data Persistenceβ
β Routes & Middle β β Error Handling β
β JSON Responses β β Production Readyβ
βββββββββββββββββββ βββββββββββββββββββ
API Foundation Database Ready
0:00 - 1:00
The Full-Stack JavaScript Advantage
Frontend: JavaScript (React)
Backend: PHP/Python/Java/C#
Database: MySQL/PostgreSQL
Problems:
β Different languages
β Context switching
β Different deployment
β Different tools/debugging
Frontend: JavaScript (React)
Backend: JavaScript (Node.js)
Database: MySQL/PostgreSQL
Benefits:
β
Same language everywhere
β
Shared code/utilities
β
Single runtime
β
Unified tooling
Real-World Success Stories: - Netflix: Migrated from Java to Node.js β 70% reduction in startup time - Uber: Real-time matching system built on Node.js - LinkedIn: Mobile API backend switched to Node.js β 10x performance - PayPal: 2x faster development, 33% fewer lines of code
Why Express? The minimal, unopinionated web framework that powers 60%+ of Node.js web applications.
Letβs explore our starting point
Whatβs included:
backend/
βββ src/
β βββ app.js β Express app setup
β βββ routes/ β API route handlers
β β βββ products.js β Product endpoints
β βββ middleware/ β Custom middleware
β βββ config/ β Database config
β βββ models/ β Database models (later)
βββ package.json β Dependencies
βββ server.js β Server entry point
Start the backend:
Test it works:
Understanding the Express foundation
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const app = express();
// Middleware
app.use(cors()); // Enable CORS for React frontend
app.use(morgan('dev')); // HTTP request logging
app.use(express.json()); // Parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Parse form data
// Routes
app.use('/api/products', require('./routes/products'));
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString });
});
// Catch-all error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
module.exports = app;
Key Concepts: - Middleware: Functions that process requests before reaching routes - Routes: Define API endpoints and their handlers - Error Handling: Centralized error processing
RESTful APIs follow predictable patterns
Method | Purpose | Success Status | Example |
---|---|---|---|
GET | Retrieve data | 200 OK | Get all products |
POST | Create new resource | 201 Created | Create product |
PUT | Update entire resource | 200 OK | Update product |
PATCH | Update partial resource | 200 OK | Update product name |
DELETE | Remove resource | 200 OK or 204 No Content | Delete product |
// Resource collections
GET /api/products // Get all products
POST /api/products // Create new product
// Specific resources
GET /api/products/:id // Get one product
PUT /api/products/:id // Update product
DELETE /api/products/:id // Delete product
// Nested resources
GET /api/products/:id/reviews // Get product reviews
POST /api/products/:id/reviews // Add review to product
// Success response
{
"success": true,
"data": { /* actual data */ },
"message": "Optional success message"
}
// Error response
{
"success": false,
"error": "Error message",
"details": { /* optional error details */ }
}
// Collection response
{
"success": true,
"data": [ /* array of items */ ],
"meta": {
"total": 150,
"page": 1,
"limit": 20
}
}
Letβs build the review aggregator API endpoints
const express = require('express');
const router = express.Router();
// Mock data (will replace with database)
let products = [
{
id: 1,
name: "Wireless Bluetooth Headphones",
description: "Premium noise-canceling headphones",
price: 199.99,
category: "Electronics"
},
{
id: 2,
name: "Smart Fitness Tracker",
description: "Track your daily activities and health metrics",
price: 149.99,
category: "Electronics"
}
];
let reviews = []; // Will store fetched reviews
let nextId = 3;
// GET /api/products - Get all products
router.get('/', (req, res) => {
try {
res.json({
success: true,
data: products,
meta: {
total: products.length
}
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch products'
});
}
});
// GET /api/products/:id - Get specific product
router.get('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
const product = products.find(p => p.id === id);
if (!product) {
return res.status(404).json({
success: false,
error: 'Product not found'
});
}
res.json({
success: true,
data: product
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch product'
});
}
});
// POST /api/products - Create new product
router.post('/', (req, res) => {
try {
const { name, description, price, category } = req.body;
// Validation
if (!name || !price) {
return res.status(400).json({
success: false,
error: 'Name and price are required'
});
}
const newProduct = {
id: nextId++,
name,
description: description || '',
price: parseFloat(price),
category: category || 'Uncategorized'
};
products.push(newProduct);
res.status(201).json({
success: true,
data: newProduct,
message: 'Product created successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to create product'
});
}
});
module.exports = router;
Generate Review API Endpoints
Prompt for AI:
Context: Express.js REST API for product review aggregator
Task: Add review endpoints to existing products router
Endpoints needed:
1. POST /api/products/:id/fetch - Simulate fetching reviews from external scraper
2. GET /api/products/:id/reviews - Get all reviews for a product
3. GET /api/products/:id/aggregate - Get review statistics
Mock data structure for reviews:
{
id: number,
productId: number,
source: "Amazon" | "BestBuy" | "Walmart",
author: string,
rating: number (1-5),
title: string,
content: string,
createdAt: date
}
Requirements:
- Proper error handling and status codes
- JSON response format with success/error pattern
- Input validation for POST requests
- Mock external API delay (1-2 seconds)
- Calculate aggregate stats (average rating, count by source)
Output: Complete Express router code to add to products.js
After AI generates: Test endpoints with curl or Postman
Letβs test our API with different tools
# Get all products
curl http://localhost:4000/api/products
# Get specific product
curl http://localhost:4000/api/products/1
# Create new product
curl -X POST http://localhost:4000/api/products \
-H "Content-Type: application/json" \
-d '{
"name": "Gaming Keyboard",
"description": "RGB mechanical keyboard",
"price": 89.99,
"category": "Electronics"
}'
# Fetch reviews for product
curl -X POST http://localhost:4000/api/products/1/fetch
# Get reviews
curl http://localhost:4000/api/products/1/reviews
# Get review stats
curl http://localhost:4000/api/products/1/aggregate
Create test.http
file:
### Get all products
GET http://localhost:4000/api/products
### Get specific product
GET http://localhost:4000/api/products/1
### Create new product
POST http://localhost:4000/api/products
Content-Type: application/json
{
"name": "Smart Watch",
"description": "Fitness tracking smartwatch",
"price": 299.99,
"category": "Electronics"
}
### Fetch reviews
POST http://localhost:4000/api/products/1/fetch
β Hour 1 Checkpoint
You should now have: - β Express server running on port 4000 - β Product CRUD endpoints working - β Review endpoints (fetch, get, aggregate) - β Proper error handling and JSON responses - β API testing experience with curl/REST client
Hour 2 Preview: Database integration, data persistence, production patterns!
1:00 - 2:00
Current Problem: In-Memory Data
// Current approach (loses data on restart!)
let products = [...];
let reviews = [...];
// Problems:
β Data lost when server restarts
β No data sharing between server instances
β No concurrent access protection
β No data relationships
β No backup/recovery
β Not scalable
Database Solution: Persistent Data
-- Data survives server restarts
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Benefits:
β
Data persistence
β
ACID transactions
β
Concurrent access
β
Data relationships
β
Backup/recovery
β
Scalable queries
Why MySQL? - Industry standard: Used by Facebook, YouTube, Twitter - Mature ecosystem: 25+ years of development - Excellent performance: Optimized for web applications - Great tooling: phpMyAdmin, MySQL Workbench, CLI tools - Cloud ready: Available on AWS RDS, Google Cloud SQL, etc.
Database setup in fullstack-minimal-app
Create .env
file in backend:
// config/database.js
const mysql = require('mysql2/promise');
require('dotenv').config();
const dbConfig = {
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,
queueLimit: 0
};
const pool = mysql.createPool(dbConfig);
// Test connection
async function testConnection() {
try {
const connection = await pool.getConnection();
console.log('β
Database connected successfully');
connection.release();
} catch (error) {
console.error('β Database connection failed:', error.message);
process.exit(1);
}
}
module.exports = { pool, testConnection };
Design tables for our review aggregator
-- Create database
CREATE DATABASE IF NOT EXISTS review_aggregator;
USE review_aggregator;
-- Products table
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
category VARCHAR(100) DEFAULT 'Electronics',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Reviews table
CREATE TABLE reviews (
id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT NOT NULL,
source VARCHAR(50) NOT NULL,
external_review_id VARCHAR(100),
author VARCHAR(255),
rating INT CHECK (rating BETWEEN 1 AND 5),
title VARCHAR(500),
content TEXT,
review_date DATETIME,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
UNIQUE KEY unique_review (product_id, source, external_review_id)
);
-- Insert sample data
INSERT INTO products (name, description, price, category) VALUES
('Wireless Bluetooth Headphones', 'Premium noise-canceling headphones with 30-hour battery', 199.99, 'Electronics'),
('Smart Fitness Tracker', 'Advanced health and fitness monitoring device', 149.99, 'Electronics'),
('Gaming Mechanical Keyboard', 'RGB backlit mechanical keyboard for gaming', 89.99, 'Electronics');
Replace in-memory data with MySQL queries
// routes/products.js
const express = require('express');
const { pool } = require('../config/database');
const router = express.Router();
// GET /api/products - Get all products
router.get('/', async (req, res) => {
try {
const [rows] = await pool.execute(
'SELECT * FROM products ORDER BY created_at DESC'
);
res.json({
success: true,
data: rows,
meta: {
total: rows.length
}
});
} catch (error) {
console.error('Database error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch products'
});
}
});
// GET /api/products/:id - Get specific product
router.get('/:id', async (req, res) => {
try {
const [rows] = await pool.execute(
'SELECT * FROM products WHERE id = ?',
[req.params.id]
);
if (rows.length === 0) {
return res.status(404).json({
success: false,
error: 'Product not found'
});
}
res.json({
success: true,
data: rows[0]
});
} catch (error) {
console.error('Database error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch product'
});
}
});
// POST /api/products - Create new product
router.post('/', async (req, res) => {
try {
const { name, description, price, category } = req.body;
// Validation
if (!name || !price) {
return res.status(400).json({
success: false,
error: 'Name and price are required'
});
}
const [result] = await pool.execute(
'INSERT INTO products (name, description, price, category) VALUES (?, ?, ?, ?)',
[name, description || '', parseFloat(price), category || 'Electronics']
);
// Get the created product
const [newProduct] = await pool.execute(
'SELECT * FROM products WHERE id = ?',
[result.insertId]
);
res.status(201).json({
success: true,
data: newProduct[0],
message: 'Product created successfully'
});
} catch (error) {
console.error('Database error:', error);
res.status(500).json({
success: false,
error: 'Failed to create product'
});
}
});
Generate Database-Backed Review Endpoints
Prompt for AI:
Context: Express.js API with MySQL database integration
Task: Create review endpoints that persist data to MySQL
Database schema:
- reviews table with columns: id, product_id, source, external_review_id, author, rating, title, content, review_date, fetched_at
- Foreign key relationship to products table
Endpoints needed:
1. POST /api/products/:id/fetch - Simulate fetching and storing reviews
2. GET /api/products/:id/reviews - Get reviews from database with pagination
3. GET /api/products/:id/aggregate - Calculate stats from database
4. DELETE /api/products/:id/reviews/:reviewId - Delete a review
Requirements:
- Use mysql2/promise with prepared statements
- Proper error handling for database operations
- Prevent duplicate reviews (UNIQUE constraint handling)
- Input validation and sanitization
- Paginated responses for GET /reviews (limit/offset)
- Aggregate calculations using SQL GROUP BY
Mock external API simulation:
- Generate 5-10 random reviews per fetch
- Different sources (Amazon, BestBuy, Walmart)
- Random ratings and realistic review content
Output: Complete Express router code with MySQL integration
Test the generated code: Create reviews, fetch them, check aggregates
Making your API production-ready
const { body, param, validationResult } = require('express-validator');
// Validation middleware
const validateProduct = [
body('name')
.isString()
.isLength({ min: 1, max: 255 })
.trim()
.escape(),
body('price')
.isFloat({ min: 0 })
.toFloat(),
body('description')
.optional()
.isString()
.isLength({ max: 1000 })
.trim()
.escape(),
body('category')
.optional()
.isString()
.isLength({ max: 100 })
.trim()
];
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: 'Validation failed',
details: errors.array()
});
}
next();
};
// Usage in routes
router.post('/', validateProduct, handleValidationErrors, async (req, res) => {
// Safe to use req.body - it's validated and sanitized
});
// For operations that need to be atomic
router.post('/:id/fetch', async (req, res) => {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 1. Check if product exists
const [products] = await connection.execute(
'SELECT id FROM products WHERE id = ?',
[req.params.id]
);
if (products.length === 0) {
throw new Error('Product not found');
}
// 2. Simulate fetching reviews from external API
const mockReviews = generateMockReviews(req.params.id);
// 3. Insert reviews (ignoring duplicates)
for (const review of mockReviews) {
await connection.execute(
`INSERT IGNORE INTO reviews
(product_id, source, external_review_id, author, rating, title, content, review_date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
review.product_id,
review.source,
review.external_review_id,
review.author,
review.rating,
review.title,
review.content,
review.review_date
]
);
}
await connection.commit();
res.json({
success: true,
message: `Fetched ${mockReviews.length} new reviews`,
data: { count: mockReviews.length }
});
} catch (error) {
await connection.rollback();
console.error('Transaction error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch reviews'
});
} finally {
connection.release();
}
});
const rateLimit = require('express-rate-limit');
// Apply rate limiting to expensive operations
const fetchLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // limit each IP to 10 requests per windowMs
message: {
success: false,
error: 'Too many fetch requests, please try again later'
}
});
router.post('/:id/fetch', fetchLimiter, async (req, res) => {
// Protected endpoint
});
What Youβve Built Today:
Next Steps: Practice session to build the complete review aggregator backend, then integrate with React frontend!
Immediately after this lecture: 1-hour hands-on practice building your complete review aggregator backend using the patterns learned today.
What youβll build: - Complete Express API with MySQL persistence - Review fetching and aggregation endpoints - Production-ready error handling and validation - Database schema and sample data - API testing and documentation
This connects to: Previous sessionβs React frontend - youβll have a working full-stack application!
Key Achievements: - π Built REST APIs with Express.js - ποΈ Integrated MySQL database persistence - π Implemented production-ready patterns - π§ͺ Tested APIs with multiple tools - π€ Used AI for rapid development - ποΈ Designed maintainable code architecture
Next Steps: Complete the backend practice, then connect it to your React frontend for a full-stack application! π
E. Bruno - Node.js & Express Backend Development