This guide lists specific improvements you can make to enhance the Minimal Product Catalog app. Each improvement includes:
- 🎯 Learning objectives - What you'll learn by implementing it
- ⏱️ Estimated time - How long it should take
- 🛠️ Step-by-step instructions - Exactly what to do
- 🧪 How to test - Verify your implementation works
Time: 15 minutes | Skill Level: Beginner
What you'll learn: HTTP security headers, middleware patterns, production security
Steps:
- Install Helmet:
npm install --workspace=backend helmet - Add to
backend/src/index.jsafter other imports:import helmet from 'helmet'; // Add this before your routes app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], imgSrc: ["'self'", "https:", "data:", "placehold.co"], // Allow Vite dev server in development ...(process.env.NODE_ENV === 'development' && { scriptSrc: ["'self'", "'unsafe-inline'"], connectSrc: ["'self'", "http://localhost:5173"], }) } } }));
Test: Check response headers in browser dev tools - you should see X-Frame-Options, X-Content-Type-Options, etc.
Time: 20 minutes | Skill Level: Beginner
What you'll learn: API protection, DoS prevention, middleware composition
Steps:
- Install:
npm install --workspace=backend express-rate-limit - Create
backend/src/middleware/rateLimiter.js:import rateLimit from 'express-rate-limit'; export const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again later.', standardHeaders: true, legacyHeaders: false, }); export const writeLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20, message: 'Too many write operations, please slow down.', });
- Add to
backend/src/index.js:import { apiLimiter, writeLimiter } from './middleware/rateLimiter.js'; app.use('/api', apiLimiter); app.use('/api/products', (req, res, next) => { if (['POST', 'PUT', 'DELETE'].includes(req.method)) { return writeLimiter(req, res, next); } next(); });
Test: Make 101 rapid requests to any API endpoint - you should get rate limited.
Time: 10 minutes | Skill Level: Beginner
What you'll learn: Environment configuration, error handling, startup validation
Steps:
Add this to the top of backend/src/index.js (after imports, before other code):
// Validate required environment variables
const requiredEnvVars = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
const missing = requiredEnvVars.filter(v => !process.env[v]);
if (missing.length > 0) {
console.error(`❌ Missing required environment variables: ${missing.join(', ')}`);
console.error(' Copy .env.example to .env and fill in values');
process.exit(1);
}Test: Rename your .env file and start the backend - it should exit with a clear error message.
Time: 45 minutes | Skill Level: Intermediate
What you'll learn: React Testing Library, component testing, mocking APIs
Steps:
-
Install dependencies:
npm install --workspace=frontend --save-dev vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
-
Add to
frontend/package.jsonscripts:{ "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest --coverage" } } -
Create
frontend/vite.config.jstest configuration:import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], test: { globals: true, environment: 'jsdom', setupFiles: ['./src/test/setup.js'], }, })
-
Create
frontend/src/test/setup.js:import '@testing-library/jest-dom'
-
Create your first test
frontend/src/components/__tests__/ProductList.test.jsx:import { render, screen } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import ProductList from '../ProductList'; const renderWithRouter = (component) => { return render(<BrowserRouter>{component}</BrowserRouter>); }; describe('ProductList', () => { it('shows loading state initially', () => { renderWithRouter( <ProductList categories={[]} backend="http://test" backendAvailable={true} /> ); expect(screen.getByText(/loading products/i)).toBeInTheDocument(); }); it('shows backend unavailable message', () => { renderWithRouter( <ProductList categories={[]} backend="http://test" backendAvailable={false} /> ); expect(screen.getByText(/backend is currently unavailable/i)).toBeInTheDocument(); }); });
Test: Run npm run test --workspace=frontend - tests should pass.
Time: 15 minutes | Skill Level: Beginner
What you'll learn: Dependency management, security updates, version compatibility
Steps:
- Check for updates:
npm outdated - Update backend:
npm update --workspace=backend - Update frontend:
npm update --workspace=frontend - Test everything still works:
npm run lint && npm test - Test in Docker:
docker compose up --build
Test: All tests pass, app runs without errors.
Time: 60 minutes | Skill Level: Intermediate
What you'll learn: Separation of concerns, testable business logic, layered architecture
Steps:
-
Create
backend/src/services/ProductService.js:import { AppError } from '../middleware/errorHandler.js'; export class ProductService { constructor(db) { this.db = db; } async createProduct({ name, description, price, image_url, category_id }) { const [result] = await this.db.query( `INSERT INTO products (name, description, price, image_url, category_id) VALUES (?, ?, ?, ?, ?)`, [name, description || null, price, image_url || null, category_id || null] ); return this.getProductById(result.insertId); } async getProductById(id) { const [rows] = await this.db.query( `SELECT p.*, c.id as c_id, c.name as c_name FROM products p LEFT JOIN categories c ON p.category_id = c.id WHERE p.id = ?`, [id] ); if (rows.length === 0) { throw new AppError('Product not found', 404); } return rows[0]; } async deleteProduct(id) { const [result] = await this.db.query('DELETE FROM products WHERE id = ?', [id]); if (result.affectedRows === 0) { throw new AppError('Product not found', 404); } } }
-
Update
backend/src/routes/products.jsto use the service:import { ProductService } from '../services/ProductService.js'; const productService = new ProductService(pool); router.post('/', validateProductCreate, asyncHandler(async (req, res) => { const product = await productService.createProduct(req.body); res.status(201).json(product); }));
Test: All API endpoints work the same, but now business logic is separated.
Time: 30 minutes | Skill Level: Intermediate
What you'll learn: Custom hooks, code reusability, React patterns
Steps:
-
Create
frontend/src/hooks/useFetch.js:import { useState, useEffect } from 'react'; export function useFetch(url, options = {}) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function fetchData() { setLoading(true); try { const res = await fetch(url, options); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); if (!cancelled) { setData(json); setError(null); } } catch (err) { if (!cancelled) { setError(err); } } finally { if (!cancelled) { setLoading(false); } } } fetchData(); return () => { cancelled = true }; }, [url, JSON.stringify(options)]); return { data, loading, error }; }
-
Use it in components to reduce boilerplate.
Test: Components using the hook work the same as before.
Time: 30 minutes | Skill Level: Beginner
What you'll learn: Production deployment, security considerations, DevOps basics
Create DEPLOYMENT.md with production deployment checklist and security guidelines.
Time: 10 minutes | Skill Level: Beginner
What you'll learn: Docker management, shell scripting, cleanup automation
Create scripts/clean-docker.sh to reset your development environment.
Time: 120 minutes | Skill Level: Advanced
What you'll learn: Type safety, modern development practices, interface design
Convert the frontend to TypeScript with proper type definitions.
Time: 90 minutes | Skill Level: Advanced
What you'll learn: End-to-end testing, browser automation, integration testing
Test complete user workflows automatically.
Time: 45 minutes | Skill Level: Intermediate
What you'll learn: Database version control, team development, schema evolution
Implement a proper migration system for database changes.
This guide is designed to help you learn by doing. Each improvement builds real-world skills you'll use as a professional developer.