Build a Pharmacy E-Commerce API

Connect a React frontend to a Node.js/Express API backed by PostgreSQL.

Introduction

30 Second Summary

Every time you order from an online pharmacy, three layers of software work together to show you the right products. A database stores inventory, a server delivers data on request, and a frontend displays it all in your browser.

In this project, you will build all three layers from scratch using Node.js, Express.js, PostgreSQL, and React. You will end up with a working pharmacy product catalog that customers can browse and filter by category.

What You'll Build

A full-stack pharmacy e-commerce application with a Node.js/Express REST API connected to a PostgreSQL database, and a React frontend that displays products with category filtering.

By the end of this project, you'll have:

  • A PostgreSQL database with a products table seeded with pharmacy items.
  • An Express.js REST API with endpoints to list, filter, and retrieve products.
  • A React frontend displaying a responsive product grid with category filters.
  • Secret Mission: Add a shopping cart with add/remove functionality and a cart summary sidebar.

Are there any prerequisites?

This project assumes basic JavaScript knowledge and familiarity with the terminal. You do not need any prior experience with Node.js, PostgreSQL, or React.

Set Up Your Development Environment

Building a full-stack application requires three core technologies working together: a JavaScript runtime, a database, and an editor.

In this step, you'll confirm that Node.js and PostgreSQL are ready on your machine, create the database your application will use, and set up the project folder structure.

In this step, get ready to:
  • Verify Node.js and npm are ready.
  • Start PostgreSQL and create your database.
  • Create the project folder structure.
Verify your Node.js installation

Node.js is the JavaScript runtime that powers your backend server. You need version 24 or higher for this project.

  • Check your Node.js version by running this command in your terminal:
node --version
  • You should see a version number starting with v24 (for example, v24.15.0).
  • Verify npm (Node's package manager) is also available by running:
npm --version
  • You should see a version number printed (for example, 10.x.x or higher).

What are Node.js and npm?

Node.js lets you run JavaScript outside of a browser. Your Express API server will run on Node.js.

npm is the package manager that comes bundled with Node.js. You'll use it to install libraries like Express and the PostgreSQL client.

Command not found?

If your terminal doesn't recognize node or npm, Node.js may not be installed or your PATH is not configured correctly. Visit the official Node.js downloads page at nodejs.org to install version 24 LTS.

Help me install Node.js v24 LTS on my operating system.

Start PostgreSQL and create the database

PostgreSQL is the relational database that will store your pharmacy product data. You need it running as a background service before you can create a database.

  • Verify PostgreSQL is installed by running:
psql --version
  • You should see output like psql (PostgreSQL) 17.x.

psql not found?

If your terminal doesn't recognize psql, PostgreSQL is not installed or not on your PATH. Visit postgresql.org/download for installation instructions for your operating system.

Help me install PostgreSQL 17 on my operating system.

  • Start the PostgreSQL service so it can accept connections.

macOS

  • Start the PostgreSQL service by running:
brew services start postgresql@17
  • You should see a message indicating the service started successfully.

Windows

PostgreSQL on Windows typically starts automatically as a service after installation. If it is not running:

  • Press the Windows key, type Services, and press Enter.
  • Find postgresql-x64-17 in the list.
  • Right-click it and select Start.

Linux

  • Start the PostgreSQL service by running:
sudo systemctl start postgresql
  • Confirm it is running with:
sudo systemctl status postgresql
  • You should see active (running) in the output.
  • Create the database for your pharmacy application by running:
createdb pharmacy_db
  • If the command completes with no output, the database was created successfully.

What does createdb do?

The createdb command is a PostgreSQL utility that creates a new database. It is a wrapper around the SQL CREATE DATABASE command.

Your new pharmacy_db database is currently empty. You'll add tables and data in the next step.

Permission denied or role does not exist?

On some systems, createdb needs to run as the PostgreSQL superuser. On macOS with Homebrew, the default superuser is your system username. On Linux, you may need to prefix the command with sudo -u postgres.

Help me fix this createdb permission error.

Create the project folder structure

Your application has two halves: a server (the API) and a client (the React frontend). Keeping them in separate folders makes development cleaner.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir pharmacy-ecommerce
cd pharmacy-ecommerce
  • Create the two subdirectories for your server and client code by running:
mkdir server client
  • Confirm the folders exist by running:
ls
  • You should see client and server in the output.
  • Open the pharmacy-ecommerce folder in VS Code by running:
code .

What is this folder structure for?

The server/ folder will hold your Express API, database connection, and seed script. The client/ folder will hold your React frontend built with Vite.

Separating them lets you install different dependencies and run each half independently.

VS Code didn't open?

If code . doesn't work, the code command may not be on your PATH. Open VS Code manually, click File, then Open Folder, and select the pharmacy-ecommerce folder on your Desktop.

Help me add the VS Code code command to my PATH.

Your environment is ready. Next up, you'll design the database schema and fill it with pharmacy product data.

Design and Create the Database

Your API needs persistent storage for product information. Without a database, product data would vanish every time the server restarts.

A relational database like PostgreSQL gives you structured tables with defined columns, data types, and constraints. In this step, you'll connect Node.js to your database, design a products table schema, and seed it with sample pharmacy data.

In this step, get ready to:
  • Set up a Node.js connection to PostgreSQL using environment variables.
  • Create and seed a products table with pharmacy item data.
  • Verify the seed data with a SQL query.
Set up the database connection

The Node.js pg library connects to PostgreSQL using credentials like host, port, database name, and password. Storing these in an environment file keeps sensitive values out of your source code.

  • Switch back to your terminal.
  • Navigate into the server/ directory by running this command:
cd server
  • Initialize a Node.js project and install the database packages by running these commands:
npm init -y
npm install pg dotenv

What do these packages do?

  • pg is the PostgreSQL client for Node.js. It provides a connection pool to run SQL queries.
  • dotenv loads variables from a .env file into process.env, keeping credentials separate from code.
  • You should see output confirming both packages were added to your package.json.

Seeing permission errors?

Make sure you ran cd server first. The npm init command must run inside the server/ folder, not the project root.

help me fix npm install errors in my server directory

  • Create a new file called .env inside your server/ folder and add this configuration:
PORT=3001
PGUSER=postgres
PGHOST=localhost
PGDATABASE=pharmacy_db
PGPASSWORD=your-password-here
PGPORT=5432

What do these variables mean?

  • PORT sets the port your API server will listen on later.
  • PGUSER, PGHOST, PGDATABASE, and PGPORT tell the pg library where your PostgreSQL instance is running.
  • PGPASSWORD is the password you set when installing PostgreSQL. Replace your-password-here with your actual PostgreSQL password.
  • Save .env.
  • Create a new file called db.js inside your server/ folder and add this code:
const { Pool } = require('pg');
require('dotenv').config();

// Create a connection pool using environment variables
const pool = new Pool({
  user: process.env.PGUSER,
  host: process.env.PGHOST,
  database: process.env.PGDATABASE,
  password: process.env.PGPASSWORD,
  port: parseInt(process.env.PGPORT, 10),
});

module.exports = pool;

What does this code do?

A Pool manages multiple database connections at once. Instead of opening and closing a connection for every query, the pool keeps connections ready and reuses them.

By exporting pool, any file in your server can import this module and run queries against pharmacy_db without duplicating connection logic.

  • Save db.js.
  • Confirm both files exist by running:
ls -a
  • You should see .env, db.js, node_modules, package.json, and package-lock.json in the output.

Missing .env in the list?

Files starting with a dot are hidden by default. The -a flag in ls -a shows them. If you only ran ls without -a, run it again with the flag.

help me check if my .env and db.js files were created correctly

Write and run the seed script

A seed script creates your database tables and fills them with initial data. This lets you reset your database to a known state at any time by re-running the script.

  • Create a new file called seed.js inside your server/ folder and add this code:
const pool = require('./db');

// SQL statement to create the products table
const createTable = `
  CREATE TABLE IF NOT EXISTS products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    category VARCHAR(100) NOT NULL,
    stock INTEGER NOT NULL DEFAULT 0,
    requires_prescription BOOLEAN DEFAULT false,
    image_url VARCHAR(500),
    created_at TIMESTAMP DEFAULT NOW()
  );
`;

What does this schema define?

  • id SERIAL PRIMARY KEY auto-generates a unique number for each product.
  • price DECIMAL(10, 2) stores prices with exactly two decimal places.
  • requires_prescription BOOLEAN flags products that need a doctor's prescription.
  • CREATE TABLE IF NOT EXISTS prevents errors if the table already exists from a previous run.

You will not see a change yet. The next chunk adds the product data that fills this table.

  • Below the createTable variable in seed.js, add the product data:
// SQL statement to insert sample pharmacy products
const seedProducts = `
  INSERT INTO products (name, description, price, category, stock, requires_prescription, image_url)
  VALUES
    ('Ibuprofen 200mg', 'Pain relief tablets, pack of 24', 5.99, 'Pain Relief', 150, false, '/images/ibuprofen.jpg'),
    ('Paracetamol 500mg', 'Fever and pain relief, pack of 32', 3.49, 'Pain Relief', 200, false, '/images/paracetamol.jpg'),
    ('Vitamin D3 1000IU', 'Daily vitamin D supplement, 90 capsules', 8.99, 'Vitamins', 75, false, '/images/vitamin-d.jpg'),
    ('Vitamin C 500mg', 'Immune support chewable tablets, 60 count', 6.49, 'Vitamins', 120, false, '/images/vitamin-c.jpg'),
    ('First Aid Kit', 'Complete home first aid kit with bandages and antiseptic', 15.99, 'First Aid', 45, false, '/images/first-aid-kit.jpg'),
    ('Adhesive Bandages', 'Assorted sizes, waterproof, box of 50', 4.29, 'First Aid', 300, false, '/images/bandages.jpg'),
    ('Hydrocortisone Cream 1%', 'Anti-itch cream for minor skin irritations, 30g', 7.49, 'Skin Care', 60, false, '/images/hydrocortisone.jpg'),
    ('Sunscreen SPF 50', 'Broad spectrum sun protection, 200ml', 12.99, 'Skin Care', 80, false, '/images/sunscreen.jpg'),
    ('Amoxicillin 500mg', 'Antibiotic capsules, 21 count', 12.50, 'Antibiotics', 30, true, '/images/amoxicillin.jpg'),
    ('Omeprazole 20mg', 'Acid reflux relief capsules, 28 count', 9.99, 'Digestive Health', 90, false, '/images/omeprazole.jpg')
  ON CONFLICT DO NOTHING;
`;

What does ON CONFLICT DO NOTHING mean?

This clause tells PostgreSQL to skip any row that would cause a conflict (like a duplicate primary key). It makes the script safe to run multiple times without creating duplicate products.

You will not see a change yet. The next chunk adds the function that runs both queries.

  • Below the seedProducts variable in seed.js, add the function that executes both queries:
// Run the create table and seed queries
async function seed() {
  try {
    await pool.query(createTable);
    console.log('Products table created successfully');
    await pool.query(seedProducts);
    console.log('Seed data inserted successfully');
  } catch (err) {
    console.error('Error seeding database:', err.message);
  } finally {
    await pool.end();
  }
}

seed();

What does this function do?

  • pool.query(createTable) sends the CREATE TABLE statement to PostgreSQL.
  • pool.query(seedProducts) inserts all 10 products into the newly created table.
  • pool.end() in the finally block closes the database connection when done, whether the queries succeeded or failed.
  • Save seed.js.
  • Run the seed script by running this command:
node seed.js
  • You should see two success messages:
Products table created successfully
Seed data inserted successfully

Seeing a connection error?

If you see a message about authentication failing, check that the PGPASSWORD value in your .env file matches the password you set when installing PostgreSQL. Also confirm that the PostgreSQL service is running.

If you see a message about the database not existing, make sure PGDATABASE in .env is set to pharmacy_db (the database you created in Step 1).

help me fix my seed.js connection error

✔️ Awesome, I've got everything!

Great. Double check you have saved seed.js and move on to verify the data.

ⓧ I'd like to double check the full code

const pool = require('./db');

const createTable = `
  CREATE TABLE IF NOT EXISTS products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    category VARCHAR(100) NOT NULL,
    stock INTEGER NOT NULL DEFAULT 0,
    requires_prescription BOOLEAN DEFAULT false,
    image_url VARCHAR(500),
    created_at TIMESTAMP DEFAULT NOW()
  );
`;

const seedProducts = `
  INSERT INTO products (name, description, price, category, stock, requires_prescription, image_url)
  VALUES
    ('Ibuprofen 200mg', 'Pain relief tablets, pack of 24', 5.99, 'Pain Relief', 150, false, '/images/ibuprofen.jpg'),
    ('Paracetamol 500mg', 'Fever and pain relief, pack of 32', 3.49, 'Pain Relief', 200, false, '/images/paracetamol.jpg'),
    ('Vitamin D3 1000IU', 'Daily vitamin D supplement, 90 capsules', 8.99, 'Vitamins', 75, false, '/images/vitamin-d.jpg'),
    ('Vitamin C 500mg', 'Immune support chewable tablets, 60 count', 6.49, 'Vitamins', 120, false, '/images/vitamin-c.jpg'),
    ('First Aid Kit', 'Complete home first aid kit with bandages and antiseptic', 15.99, 'First Aid', 45, false, '/images/first-aid-kit.jpg'),
    ('Adhesive Bandages', 'Assorted sizes, waterproof, box of 50', 4.29, 'First Aid', 300, false, '/images/bandages.jpg'),
    ('Hydrocortisone Cream 1%', 'Anti-itch cream for minor skin irritations, 30g', 7.49, 'Skin Care', 60, false, '/images/hydrocortisone.jpg'),
    ('Sunscreen SPF 50', 'Broad spectrum sun protection, 200ml', 12.99, 'Skin Care', 80, false, '/images/sunscreen.jpg'),
    ('Amoxicillin 500mg', 'Antibiotic capsules, 21 count', 12.50, 'Antibiotics', 30, true, '/images/amoxicillin.jpg'),
    ('Omeprazole 20mg', 'Acid reflux relief capsules, 28 count', 9.99, 'Digestive Health', 90, false, '/images/omeprazole.jpg')
  ON CONFLICT DO NOTHING;
`;

async function seed() {
  try {
    await pool.query(createTable);
    console.log('Products table created successfully');
    await pool.query(seedProducts);
    console.log('Seed data inserted successfully');
  } catch (err) {
    console.error('Error seeding database:', err.message);
  } finally {
    await pool.end();
  }
}

seed();
Verify the seed data

The seed script reported success, but let's confirm the data actually landed in PostgreSQL by querying the table directly.

  • Connect to your pharmacy_db database by running this command:
psql -d pharmacy_db
  • You should see a prompt like pharmacy_db=> or pharmacy_db=#.
  • Run a SELECT query to see your products by typing this at the psql prompt:
SELECT id, name, price, category FROM products;
  • You should see a table with 10 rows showing product names, prices, and categories like Pain Relief, Vitamins, First Aid, and Skin Care.
  • Exit psql by typing:
\q

Seeing zero rows or a missing table error?

If you see the message "relation products does not exist", the seed script did not create the table. Go back and check the output from node seed.js. If it showed an error message, fix your .env credentials and run the seed script again.

If you see the table but zero rows, the INSERT may have been skipped. Delete the empty table with DROP TABLE products; inside psql, exit, then re-run node seed.js.

help me troubleshoot my seed.js not inserting data

Your database is designed, created, and filled with pharmacy products. Next up, you'll build the Express.js API that serves this data to the frontend.

Build the Express.js API

Your database is loaded with pharmacy products, but a web browser cannot query PostgreSQL directly. You need an intermediary that listens for web requests and fetches the right data from the database.

That intermediary is a REST API. In this step, you'll build one using Express.js, a lightweight web framework for Node.js that handles routing and middleware.

In this step, get ready to:
  • Initialize the server project and install dependencies.
  • Create an Express server with JSON parsing and CORS middleware.
  • Define three API endpoints for products and categories.
Initialize the server project and install dependencies

Before writing any server code, you need a package.json file that defines your project's metadata and dependencies. This is what npm uses to know which packages to install.

  • In VS Code, open the integrated terminal by pressing Cmd+` (macOS) or Ctrl+` (Windows).
  • Navigate into the server/ directory by running this command:
cd server
  • Create a new file called package.json in the server/ folder and paste the following content:
{
  "name": "pharmacy-api",
  "version": "1.0.0",
  "description": "Pharmacy e-commerce API",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "seed": "node seed.js"
  },
  "dependencies": {
    "cors": "2.8.6",
    "dotenv": "16.5.0",
    "express": "5.1.0",
    "pg": "8.21.0"
  }
}

What does this file do?

  • scripts defines shortcut commands. npm start runs your server, and npm run seed runs your seed script.
  • dependencies lists the four packages your API needs: express (web framework), pg (PostgreSQL client), cors (cross-origin support), and dotenv (environment variable loading).
  • Save package.json.
  • Install all four dependencies by running this command:
npm install
  • Confirm the installation succeeded by running:
ls node_modules

You should see folders for express, pg, cors, and dotenv among the listed directories.

Seeing errors from npm install?

  • Make sure you saved package.json before running the install command.
  • Check that your terminal is inside the server/ directory by running pwd. You should see a path ending in /server.
  • Verify your JSON syntax has no trailing commas or missing quotes. Help me debug my npm install error
Create the Express server with middleware

Express needs a main entry file that creates the app, attaches middleware, and starts listening for requests. The db.js module you created in the previous step handles the PostgreSQL connection, so your server can import it directly.

  • Create a new file called server.js in the server/ folder and paste this code:
const express = require('express');
const cors = require('cors');
const pool = require('./db');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3001;

// Enable cross-origin requests from the React frontend
app.use(cors());
// Parse incoming JSON request bodies
app.use(express.json());

app.listen(PORT, () => {
  console.log(`Pharmacy API running on http://localhost:${PORT}`);
});

What does this code do?

  • require('./db') imports the database pool from db.js. Every route handler uses this pool to query PostgreSQL.
  • cors() allows your React frontend (running on a different port) to make requests to this API without being blocked by the browser.
  • express.json() automatically parses JSON data from incoming request bodies so you can access it in route handlers.
  • app.listen starts the server on port 3001 (read from the .env file).

Why CORS?

Browsers block requests between different origins by default. Your React app will run on localhost:5173 and your API on localhost:3001. Without CORS enabled, the browser would reject every API call.

  • Save server.js.
  • Start the server by running:
node server.js

You should see Pharmacy API running on http://localhost:3001 printed in your terminal.

  • Stop the server by pressing Ctrl+C in the terminal. You'll add routes before starting it again.

Server not starting?

  • If you see a "Cannot find module" error, make sure you ran npm install before starting the server.
  • If you see a connection error related to PostgreSQL, confirm your .env file has the correct password and that the PostgreSQL service is still running.
  • Help me fix my Express server startup error
Define the API routes

An API route maps an HTTP method and URL path to a function that handles the request. You'll add three routes that let the frontend list products, fetch a single product, and get available categories.

  • In server.js, find the app.listen line near the bottom of the file.
  • Add the following route above app.listen:
app.get('/api/products', async (req, res) => {
  try {
    const { category } = req.query;
    let query = 'SELECT * FROM products';
    const values = [];

    // Add a WHERE clause if a category filter is provided
    if (category) {
      query += ' WHERE category = $1';
      values.push(category);
    }

    query += ' ORDER BY name ASC';
    const result = await pool.query(query, values);
    res.json(result.rows);
  } catch (err) {
    console.error('Error fetching products:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

What does this route do?

  • GET /api/products returns all products from the database, sorted alphabetically.
  • The optional ?category= query parameter filters results. For example, /api/products?category=Vitamins returns only vitamin products.
  • $1 is a parameterized query placeholder. It prevents SQL injection by keeping user input separate from the SQL statement.

You won't see a change yet. The next chunk adds the remaining routes, then you'll test them all together.

  • Add this second route below the /api/products route and above app.listen:
app.get('/api/products/:id', async (req, res) => {
  try {
    // Extract the product ID from the URL path
    const { id } = req.params;
    const result = await pool.query('SELECT * FROM products WHERE id = $1', [id]);

    if (result.rows.length === 0) {
      return res.status(404).json({ error: 'Product not found' });
    }

    res.json(result.rows[0]);
  } catch (err) {
    console.error('Error fetching product:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

What does this route do?

  • GET /api/products/:id fetches a single product by its database ID. The colon makes :id a dynamic URL parameter.
  • If no product matches the ID, it returns a 404 status code so the frontend knows the product doesn't exist.

One more route to add before you can test the full API.

  • Add this third route below the /api/products/:id route and above app.listen:
app.get('/api/categories', async (req, res) => {
  try {
    // Get unique category names from all products
    const result = await pool.query('SELECT DISTINCT category FROM products ORDER BY category ASC');
    res.json(result.rows.map(row => row.category));
  } catch (err) {
    console.error('Error fetching categories:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

What does this route do?

  • GET /api/categories returns a flat array of unique category names like ["Antibiotics", "Digestive Health", "First Aid", ...].
  • The .map(row => row.category) transforms the database rows into a simple string array, which is easier for the frontend to work with.
  • Save server.js.
  • Start the server again by running:
node server.js
  • Open your browser and navigate to http://localhost:3001/api/products.

You should see a JSON array of all 10 pharmacy products displayed in the browser.

  • Test the category filter by navigating to http://localhost:3001/api/products?category=Vitamins.

You should see only the two vitamin products (Vitamin D3 1000IU and Vitamin C 500mg) in the response.

Seeing an empty array or error?

  • An empty array means the database connection works but no products exist. Run node seed.js to re-seed the database.
  • A connection error means the PostgreSQL service is not running or the credentials in .env are incorrect.
  • If the page says "Cannot GET /api/products", check your route path for typos.
  • Help me debug my API route

✔️ Awesome, I've got everything!

Great. Double check you've saved server.js and that all three routes are responding correctly.

ⓧ I'd like to double check the full code

Here is the complete server.js file with all three routes:

const express = require('express');
const cors = require('cors');
const pool = require('./db');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3001;

app.use(cors());
app.use(express.json());

app.get('/api/products', async (req, res) => {
  try {
    const { category } = req.query;
    let query = 'SELECT * FROM products';
    const values = [];

    if (category) {
      query += ' WHERE category = $1';
      values.push(category);
    }

    query += ' ORDER BY name ASC';
    const result = await pool.query(query, values);
    res.json(result.rows);
  } catch (err) {
    console.error('Error fetching products:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.get('/api/products/:id', async (req, res) => {
  try {
    const { id } = req.params;
    const result = await pool.query('SELECT * FROM products WHERE id = $1', [id]);

    if (result.rows.length === 0) {
      return res.status(404).json({ error: 'Product not found' });
    }

    res.json(result.rows[0]);
  } catch (err) {
    console.error('Error fetching product:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.get('/api/categories', async (req, res) => {
  try {
    const result = await pool.query('SELECT DISTINCT category FROM products ORDER BY category ASC');
    res.json(result.rows.map(row => row.category));
  } catch (err) {
    console.error('Error fetching categories:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.listen(PORT, () => {
  console.log(`Pharmacy API running on http://localhost:${PORT}`);
});

Your API is live and serving real data from PostgreSQL. Next up, you'll build a React frontend that fetches from these endpoints and displays a polished product catalog.

Create the React Frontend

Your backend API is serving pharmacy data from PostgreSQL. Now it is time to build the frontend that customers will actually see and interact with.

You will use React to build a single-page application that fetches product data from your API and displays it in a browsable catalog. Vite handles the project scaffolding and development server.

In this step, get ready to:
  • Scaffold a React app with Vite.
  • Create a ProductCard component for displaying individual products.
  • Build the main App component with category filtering and a product grid.
Scaffold the React app with Vite

Vite is a modern build tool that scaffolds a React project in seconds. It also provides a lightning-fast development server with hot module replacement, so changes appear instantly in the browser.

  • Switch back to your terminal and navigate to your project root (the parent folder containing server/ and client/) by running:
cd ..
  • Remove the empty client/ directory so Vite can create a fresh one by running:
rm -rf client
  • Scaffold the React app by running:
npm create vite@latest client -- --template react

What does this command do?

  • npm create vite@latest runs the Vite project generator.
  • client is the folder name where the project will be created.
  • -- --template react tells Vite to use the React template, skipping the interactive template selector.
  • Move into the new client folder and install the dependencies by running:
cd client
npm install
  • Confirm the install succeeded by running:
ls node_modules

You should see a long list of package folders printed to the terminal.

Install failed or command not found?

  • Make sure you are inside the client/ directory before running npm install.
  • If npm create vite@latest was not recognized, confirm Node.js is installed by running node --version.

Help me troubleshoot my Vite scaffold or npm install.

Vite scaffolds some default files we do not need. Let's clean those up so they do not conflict with our custom styles.

  • Delete the default index.css file by running:
rm src/index.css
  • Open src/main.jsx in your code editor.
  • Find this line:
import './index.css'
  • Delete that line entirely. The file should now look like this:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
  • Save main.jsx.
Create the ProductCard component

Each product in the catalog needs its own visual card. A dedicated component keeps the rendering logic reusable and easy to maintain.

  • Create a components folder inside src/ by running:
mkdir src/components
  • Create a new file at src/components/ProductCard.jsx and paste the following code:
function ProductCard({ product }) {
  return (
    <div className="product-card">
      <div className="product-category">{product.category}</div>
      <h3 className="product-name">{product.name}</h3>
      <p className="product-description">{product.description}</p>
      <div className="product-footer">
        <span className="product-price">${Number(product.price).toFixed(2)}</span>
        <span className={`product-stock ${product.stock > 0 ? 'in-stock' : 'out-of-stock'}`}>
          {product.stock > 0 ? `${product.stock} in stock` : 'Out of stock'}
        </span>
      </div>
      {product.requires_prescription && (
        <div className="prescription-badge">Prescription Required</div>
      )}
    </div>
  );
}

export default ProductCard;

What does this component do?

  • It receives a product object as a prop and renders its details in a card layout.
  • The category badge, name, description, price, and stock status each get their own styled element.
  • A conditional prescription-badge only appears for products that require a prescription.
  • The stock status dynamically switches between in-stock and out-of-stock CSS classes based on the count.
  • Save ProductCard.jsx.

You will not see a change yet. The next chunks build the App component that uses this card.

✔️ Awesome, I've got everything!

Great. Make sure you have saved ProductCard.jsx before moving on.

ⓧ I'd like to double check the full code

function ProductCard({ product }) {
  return (
    <div className="product-card">
      <div className="product-category">{product.category}</div>
      <h3 className="product-name">{product.name}</h3>
      <p className="product-description">{product.description}</p>
      <div className="product-footer">
        <span className="product-price">${Number(product.price).toFixed(2)}</span>
        <span className={`product-stock ${product.stock > 0 ? 'in-stock' : 'out-of-stock'}`}>
          {product.stock > 0 ? `${product.stock} in stock` : 'Out of stock'}
        </span>
      </div>
      {product.requires_prescription && (
        <div className="prescription-badge">Prescription Required</div>
      )}
    </div>
  );
}

export default ProductCard;
Create the App component and styles

The main App component fetches products from your API and renders them in a responsive grid. It also provides category filter buttons so users can narrow down what they see.

  • Replace the contents of src/App.css with the following styles:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f7fa;
  color: #333;
}

.app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.header {
  text-align: center;
  padding: 30px 0;
  background: linear-gradient(135deg, #2563eb, #1e40af);
  color: white;
  border-radius: 12px;
  margin-bottom: 24px;
}

.header h1 {
  font-size: 2rem;
  margin-bottom: 8px;
}

.header p {
  opacity: 0.9;
  font-size: 1.1rem;
}

.category-filter {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 24px;
  padding: 16px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.category-filter button {
  padding: 8px 16px;
  border: 1px solid #e2e8f0;
  border-radius: 20px;
  background: white;
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.2s;
}

.category-filter button:hover {
  background: #f1f5f9;
}

.category-filter button.active {
  background: #2563eb;
  color: white;
  border-color: #2563eb;
}

.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
}

.product-card {
  background: white;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  transition: transform 0.2s, box-shadow 0.2s;
  position: relative;
}

.product-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.product-category {
  display: inline-block;
  padding: 4px 10px;
  background: #eff6ff;
  color: #2563eb;
  border-radius: 12px;
  font-size: 0.75rem;
  font-weight: 600;
  margin-bottom: 12px;
}

.product-name {
  font-size: 1.1rem;
  margin-bottom: 8px;
}

.product-description {
  color: #64748b;
  font-size: 0.9rem;
  margin-bottom: 16px;
  line-height: 1.4;
}

.product-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.product-price {
  font-size: 1.25rem;
  font-weight: 700;
  color: #1e40af;
}

.product-stock {
  font-size: 0.8rem;
  font-weight: 500;
}

.product-stock.in-stock {
  color: #16a34a;
}

.product-stock.out-of-stock {
  color: #dc2626;
}

.prescription-badge {
  margin-top: 12px;
  padding: 6px 12px;
  background: #fef3c7;
  color: #92400e;
  border-radius: 6px;
  font-size: 0.8rem;
  font-weight: 600;
  text-align: center;
}

.status-message {
  grid-column: 1 / -1;
  text-align: center;
  padding: 40px;
  color: #64748b;
  font-size: 1.1rem;
}

.status-message.error {
  color: #dc2626;
}

What do these styles do?

  • A global reset removes default browser margins and padding.
  • The .header uses a blue gradient background to brand the pharmacy storefront.
  • The .category-filter uses flexbox with wrapping so filter buttons flow across multiple rows on small screens.
  • The .product-grid uses CSS Grid with auto-fill to create a responsive layout that adjusts the number of columns automatically.
  • Product cards get a hover effect (slight lift and deeper shadow) to feel interactive.
  • Save App.css.

You will not see any changes yet because the App component still has Vite's default content. The next chunk replaces it.

✔️ Awesome, I've got everything!

Great. Confirm you saved App.css before moving on.

ⓧ I'd like to double check the full code

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f7fa;
  color: #333;
}

.app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.header {
  text-align: center;
  padding: 30px 0;
  background: linear-gradient(135deg, #2563eb, #1e40af);
  color: white;
  border-radius: 12px;
  margin-bottom: 24px;
}

.header h1 {
  font-size: 2rem;
  margin-bottom: 8px;
}

.header p {
  opacity: 0.9;
  font-size: 1.1rem;
}

.category-filter {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 24px;
  padding: 16px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.category-filter button {
  padding: 8px 16px;
  border: 1px solid #e2e8f0;
  border-radius: 20px;
  background: white;
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.2s;
}

.category-filter button:hover {
  background: #f1f5f9;
}

.category-filter button.active {
  background: #2563eb;
  color: white;
  border-color: #2563eb;
}

.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
}

.product-card {
  background: white;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  transition: transform 0.2s, box-shadow 0.2s;
  position: relative;
}

.product-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.product-category {
  display: inline-block;
  padding: 4px 10px;
  background: #eff6ff;
  color: #2563eb;
  border-radius: 12px;
  font-size: 0.75rem;
  font-weight: 600;
  margin-bottom: 12px;
}

.product-name {
  font-size: 1.1rem;
  margin-bottom: 8px;
}

.product-description {
  color: #64748b;
  font-size: 0.9rem;
  margin-bottom: 16px;
  line-height: 1.4;
}

.product-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.product-price {
  font-size: 1.25rem;
  font-weight: 700;
  color: #1e40af;
}

.product-stock {
  font-size: 0.8rem;
  font-weight: 500;
}

.product-stock.in-stock {
  color: #16a34a;
}

.product-stock.out-of-stock {
  color: #dc2626;
}

.prescription-badge {
  margin-top: 12px;
  padding: 6px 12px;
  background: #fef3c7;
  color: #92400e;
  border-radius: 6px;
  font-size: 0.8rem;
  font-weight: 600;
  text-align: center;
}

.status-message {
  grid-column: 1 / -1;
  text-align: center;
  padding: 40px;
  color: #64748b;
  font-size: 1.1rem;
}

.status-message.error {
  color: #dc2626;
}
  • Now replace the contents of src/App.jsx with the following code:
import { useState, useEffect } from 'react';
import ProductCard from './components/ProductCard';
import './App.css';

// Base URL for the Express API
const API_BASE = 'http://localhost:3001/api';

function App() {
  // State for product data, categories, and UI status
  const [products, setProducts] = useState([]);
  const [categories, setCategories] = useState([]);
  const [selectedCategory, setSelectedCategory] = useState('');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Fetch categories once on mount
  useEffect(() => {
    fetchCategories();
  }, []);

  // Re-fetch products whenever the selected category changes
  useEffect(() => {
    fetchProducts();
  }, [selectedCategory]);

  async function fetchCategories() {
    try {
      const response = await fetch(`${API_BASE}/categories`);
      if (!response.ok) throw new Error('Failed to fetch categories');
      const data = await response.json();
      setCategories(data);
    } catch (err) {
      console.error('Error fetching categories:', err);
    }
  }

  async function fetchProducts() {
    try {
      setLoading(true);
      // Build URL with optional category filter
      const url = selectedCategory
        ? `${API_BASE}/products?category=${encodeURIComponent(selectedCategory)}`
        : `${API_BASE}/products`;
      const response = await fetch(url);
      if (!response.ok) throw new Error('Failed to fetch products');
      const data = await response.json();
      setProducts(data);
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="app">
      <header className="header">
        <h1>PharmaCare Online</h1>
        <p>Your trusted online pharmacy</p>
      </header>

      <nav className="category-filter">
        <button
          className={selectedCategory === '' ? 'active' : ''}
          onClick={() => setSelectedCategory('')}
        >
          All Products
        </button>
        {categories.map(category => (
          <button
            key={category}
            className={selectedCategory === category ? 'active' : ''}
            onClick={() => setSelectedCategory(category)}
          >
            {category}
          </button>
        ))}
      </nav>

      <main className="product-grid">
        {loading && <p className="status-message">Loading products...</p>}
        {error && <p className="status-message error">Error: {error}</p>}
        {!loading && !error && products.length === 0 && (
          <p className="status-message">No products found.</p>
        )}
        {!loading && !error && products.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </main>
    </div>
  );
}

export default App;

What does this component do?

  • API_BASE points at your Express server so all fetch calls go to http://localhost:3001/api.
  • Five state variables track the products array, available categories, the currently selected category, a loading flag, and any error messages.
  • Two useEffect hooks manage data fetching. One loads categories on mount. The other reloads products whenever the selected category changes.
  • The JSX renders a header, a row of filter buttons (one per category), and a product grid that maps over the products array to render a ProductCard for each item.
  • Loading and error states display user-friendly messages while data is in transit or if something goes wrong.
  • Save App.jsx.

You will not see the app in your browser yet. In the next step, you will start both the API server and the Vite dev server to see everything come together.

Seeing a red underline or import error in your editor?

  • Make sure src/components/ProductCard.jsx exists and the filename matches the import path exactly (case-sensitive).
  • Confirm you deleted src/index.css earlier and removed its import from main.jsx. A missing file import causes build errors.

Help me fix import errors in my React app.

✔️ Awesome, I've got everything!

Great. Double check you have saved both App.jsx and App.css.

ⓧ I'd like to double check the full code

import { useState, useEffect } from 'react';
import ProductCard from './components/ProductCard';
import './App.css';

const API_BASE = 'http://localhost:3001/api';

function App() {
  const [products, setProducts] = useState([]);
  const [categories, setCategories] = useState([]);
  const [selectedCategory, setSelectedCategory] = useState('');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchCategories();
  }, []);

  useEffect(() => {
    fetchProducts();
  }, [selectedCategory]);

  async function fetchCategories() {
    try {
      const response = await fetch(`${API_BASE}/categories`);
      if (!response.ok) throw new Error('Failed to fetch categories');
      const data = await response.json();
      setCategories(data);
    } catch (err) {
      console.error('Error fetching categories:', err);
    }
  }

  async function fetchProducts() {
    try {
      setLoading(true);
      const url = selectedCategory
        ? `${API_BASE}/products?category=${encodeURIComponent(selectedCategory)}`
        : `${API_BASE}/products`;
      const response = await fetch(url);
      if (!response.ok) throw new Error('Failed to fetch products');
      const data = await response.json();
      setProducts(data);
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="app">
      <header className="header">
        <h1>PharmaCare Online</h1>
        <p>Your trusted online pharmacy</p>
      </header>

      <nav className="category-filter">
        <button
          className={selectedCategory === '' ? 'active' : ''}
          onClick={() => setSelectedCategory('')}
        >
          All Products
        </button>
        {categories.map(category => (
          <button
            key={category}
            className={selectedCategory === category ? 'active' : ''}
            onClick={() => setSelectedCategory(category)}
          >
            {category}
          </button>
        ))}
      </nav>

      <main className="product-grid">
        {loading && <p className="status-message">Loading products...</p>}
        {error && <p className="status-message error">Error: {error}</p>}
        {!loading && !error && products.length === 0 && (
          <p className="status-message">No products found.</p>
        )}
        {!loading && !error && products.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </main>
    </div>
  );
}

export default App;

Your React frontend is fully coded. Next up, you will start both servers and see your pharmacy catalog come to life in the browser.

Connect Frontend to Backend and Test

You have a React frontend and an Express.js backend sitting in separate folders. Right now they are two disconnected pieces that have never spoken to each other.

In this step, you will start both servers simultaneously and watch data flow from PostgreSQL through your API into the browser. You will also verify that the loading state, error handling, and category filters all work as expected.

In this step, get ready to:
  • Start both the Express server and the React dev server at the same time.
  • Test the full-stack flow by loading products and filtering by category.
  • Verify that loading and error states display correctly.
Start both the server and the client

Your React app already has the API address configured. The line const API_BASE = 'http://localhost:3001/api' in App.jsx tells the frontend exactly where to send its fetch requests.

Why doesn't this need a proxy?

The React dev server runs on port 5173 and the Express API runs on port 3001. Normally, browsers block requests between different origins (different ports count as different origins).

Your Express server already uses the cors middleware, which adds headers telling the browser to allow cross-origin requests. That means the React app can call the API directly without a proxy.

You need two separate terminal windows to run both servers at the same time.

  • In your first terminal, navigate to the server/ directory and start the Express API by running:
cd server
npm start
  • You should see this message in your terminal:
Pharmacy API running on http://localhost:3001

Seeing a connection error?

If you see an error about connecting to the database, make sure your PostgreSQL service is running. Also check that the password in server/.env matches the password you set for your postgres user.

Help me debug my Express server connection error

  • Open a second terminal window. Press Cmd+T (macOS) or Ctrl+Shift+T (Windows/Linux) to open a new terminal tab.
  • Navigate to the client/ directory and start the React dev server by running:
cd client
npm run dev
  • You should see Vite report that your app is running at http://localhost:5173.

Port 3001 already in use?

If you see a message that port 3001 is already in use, a previous server process may still be running. Close all terminal windows and open fresh ones before trying again.

Help me fix a port conflict on port 3001

Test the full-stack flow in the browser

Both servers are running. Time to see your full-stack application come to life.

  • Open your browser and go to http://localhost:5173.
  • You should see the PharmaCare Online header with a blue gradient background.
  • Below the header, a row of category filter buttons should appear: All Products, Antibiotics, Digestive Health, First Aid, and more.
  • The product grid should display 10 product cards with names, prices, descriptions, and stock counts.
  • Click the Pain Relief filter button.
  • The grid should narrow to show only Ibuprofen 200mg and Paracetamol 500mg.
  • Click All Products to return to the full list.

What's happening behind the scenes?

When you click a filter button, React updates the selectedCategory state. The useEffect hook watching that state fires a new fetch request to /api/products?category=Pain%20Relief.

Express receives the request, adds a WHERE clause to the SQL query, and returns only matching products. React re-renders the grid with the filtered results.

Products not showing up?

Open your browser's developer console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). Look for red error messages. If you see a network error, confirm that your Express server is still running in the first terminal tab.

If the console shows a CORS error, make sure app.use(cors()) is present in server.js and that you saved the file.

Help me debug why my React app isn't loading products from the API

Verify loading and error states

Your App.jsx already includes loading and error handling. The app shows "Loading products..." while waiting for the API response, and displays an error message if the request fails.

  • Switch to the terminal tab running your Express server.
  • Press Ctrl+C to stop the server.
  • Return to your browser and refresh the page at http://localhost:5173.
  • You should see an error message appear in red text, because the API is no longer reachable.

How does the error handling work?

The fetchProducts function wraps the API call in a try/catch block. If the fetch fails (network error or non-200 response), the catch block stores the error message in state.

The JSX then conditionally renders the error paragraph when error is not null. The finally block sets loading to false regardless of success or failure, so the loading spinner always disappears.

  • Switch back to the terminal where you stopped the server.
  • Restart the Express server by running:
npm start
  • Refresh your browser. The products should load again successfully.

Why test error states?

In production, APIs go down. Networks drop. Users hit your app during deployments. Handling errors gracefully means users see a helpful message instead of a blank screen or a cryptic crash.

✔️ Awesome, I've got everything!

Great. Make sure both your Express server and Vite dev server are running before moving on.

ⓧ I'd like to double check the full code

Here is the full App.jsx with the API connection, loading state, and error handling:

import { useState, useEffect } from 'react';
import ProductCard from './components/ProductCard';
import './App.css';

const API_BASE = 'http://localhost:3001/api';

function App() {
  const [products, setProducts] = useState([]);
  const [categories, setCategories] = useState([]);
  const [selectedCategory, setSelectedCategory] = useState('');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchCategories();
  }, []);

  useEffect(() => {
    fetchProducts();
  }, [selectedCategory]);

  async function fetchCategories() {
    try {
      const response = await fetch(`${API_BASE}/categories`);
      if (!response.ok) throw new Error('Failed to fetch categories');
      const data = await response.json();
      setCategories(data);
    } catch (err) {
      console.error('Error fetching categories:', err);
    }
  }

  async function fetchProducts() {
    try {
      setLoading(true);
      const url = selectedCategory
        ? `${API_BASE}/products?category=${encodeURIComponent(selectedCategory)}`
        : `${API_BASE}/products`;
      const response = await fetch(url);
      if (!response.ok) throw new Error('Failed to fetch products');
      const data = await response.json();
      setProducts(data);
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="app">
      <header className="header">
        <h1>PharmaCare Online</h1>
        <p>Your trusted online pharmacy</p>
      </header>

      <nav className="category-filter">
        <button
          className={selectedCategory === '' ? 'active' : ''}
          onClick={() => setSelectedCategory('')}
        >
          All Products
        </button>
        {categories.map(category => (
          <button
            key={category}
            className={selectedCategory === category ? 'active' : ''}
            onClick={() => setSelectedCategory(category)}
          >
            {category}
          </button>
        ))}
      </nav>

      <main className="product-grid">
        {loading && <p className="status-message">Loading products...</p>}
        {error && <p className="status-message error">Error: {error}</p>}
        {!loading && !error && products.length === 0 && (
          <p className="status-message">No products found.</p>
        )}
        {!loading && !error && products.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </main>
    </div>
  );
}

export default App;

Secret mission

Add a Shopping Cart

Your product catalog looks great, but customers need a way to collect items before checking out. Can you build a shopping cart that persists across page refreshes?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing cloud costs.

Resources you used:

  • PostgreSQL pharmacy_db database with the products table.
  • Express server process running on port 3001.
  • React dev server process running on port 5173.
  • Project files (server/ and client/ directories on your Desktop).

Keep everything running

No action needed. Choose this if you are still actively building on the project or plan to extend it further.

  • Your Express server and React dev server will keep running until you close their terminal windows.
  • PostgreSQL will continue running in the background as a system service.
  • Your project files remain on your Desktop, ready to pick up where you left off.

Pause - I'll come back to this later

Stop the running processes to free up memory and ports. Your files and database stay intact so you can resume later.

  • In each terminal window running the Express server and React dev server, press Ctrl+C to stop the process.
  • Stop the PostgreSQL service by running:

macOS

brew services stop postgresql

Windows

net stop postgresql-x64-17

Linux

sudo systemctl stop postgresql

When you are ready to come back, start PostgreSQL again, then run your servers with the same commands from Step 5.

Delete - I don't want to use this again

Remove all project files, the database, and stop PostgreSQL completely.

  • In each terminal window running the Express server and React dev server, press Ctrl+C to stop the process.
  • Drop the pharmacy_db database by running:
dropdb pharmacy_db
  • Remove the project files from your Desktop by running:

macOS

cd ~/Desktop
rm -rf server client

Windows

cd ~\Desktop
Remove-Item -Recurse -Force server, client

Linux

cd ~/Desktop
rm -rf server client
  • Stop the PostgreSQL service:

macOS

brew services stop postgresql

Windows

net stop postgresql-x64-17

Linux

sudo systemctl stop postgresql

If you no longer need PostgreSQL on your machine at all, you can uninstall it using your system package manager. Run brew uninstall postgresql on macOS, sudo apt remove postgresql-17 on Linux, or use the Windows uninstaller in Add or remove programs.

Nice Work!

Nice Work!

You just built a full-stack pharmacy e-commerce application from scratch. You connected a React frontend to a Node.js API backed by a PostgreSQL database, the same architecture powering production web applications at companies around the world.

You learned how to:

  • Designed a relational database schema in PostgreSQL and seeded it with product data using a reusable seed script.
  • Built a REST API with Express.js that queries PostgreSQL with parameterized queries and category filtering.
  • Created a React frontend that fetches data from the API, renders a responsive product grid, and handles loading and error states.
  • Added a shopping cart using React Context and useReducer, with localStorage persistence so cart data survives page refreshes.

Ready to quiz yourself?