Build a MERN E-Commerce Store

Build a React, Express, and MongoDB store with cart and simulated checkout.

Introduction

30 Second Summary

A smooth online shop makes every click feel immediate. Behind that simple experience, each part of the store must agree on what the shopper ordered.

In this project, you will build NovaCart using the MERN stack. You will follow one shopping journey as a React storefront sends a cart through an Express REST API to a saved MongoDB Atlas order.

What You'll Build

You'll demo a complete shopping journey in Microsoft Edge where NovaCart turns a live cart into a saved order with an NC- confirmation plus a server-calculated total.

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

  • A database-powered product catalog where you can browse four everyday products loaded from MongoDB Atlas.
  • A responsive shopping cart that keeps its quantities plus total synchronized as you shop.
  • A simulated checkout flow that displays an NC- order number plus the total calculated by the server.
  • Secret Mission: Add instant search that filters the catalog by product name or category.

Are there any prerequisites?

No previous full-stack experience is required. A Windows computer with Visual Studio Code is enough to begin because the guide covers every other setup task.

Before We Start

This step gives you a moment to define NovaCart before the hands-on work begins. You are committing to a safe MERN shopping flow that covers browsing products, managing cart quantities, and placing simulated orders without payments or authentication.

Install Node.js and Create the Client

NovaCart's client needs a JavaScript runtime. The API you build later uses the same runtime.

Node.js provides that runtime through a single Windows installation. You will use it to assemble a React client powered by Vite.

In this step, get ready to:
  • Install Node.js 24.20.0 LTS.
  • Create the Vite-powered React client files.
  • Run NovaCart in Microsoft Edge.
Install Node.js and confirm the browser

The Windows Node.js installer includes the node runtime plus the npm package manager. Start with a version check so you can follow the path that matches your computer.

  • Press the Windows key to open Windows Search.
  • Type PowerShell into the search field.
  • Press Enter to open Windows PowerShell.
  • Check whether Node.js is already installed by running this command:
node -v

What does this command do?

The node -v command asks Node.js to print its installed version. The result determines whether you can continue or need the Windows installer.

✔️ I see version 24.20.0 or higher

Your Node.js runtime meets the project requirement. Check that its bundled package manager is also available.

  • Check the installed npm version by running this command:
npm -v

What does this command do?

The npm -v command prints the installed npm version. A version number confirms that npm is ready to install NovaCart's packages.

You should see an npm version number in PowerShell.

ⓧ I see an older version

The older runtime may not support this project's Vite version. The Windows installer takes a few minutes to update Node.js plus npm.

  • Check the System type value on the Windows system information page.
  • Download node-v24.20.0-x64.msi for an x64-based computer.
  • Download node-v24.20.0-arm64.msi for an ARM-based computer.
  • Run the downloaded installer.
  • Follow the installer prompts using the default options.
  • Close the existing PowerShell window.
  • Reopen Windows PowerShell through Windows Search.
  • Confirm the updated Node.js version by running:
node -v

What should I see?

PowerShell should print v24.20.0. This confirms that the reopened terminal can access the updated runtime.

  • Confirm that npm is available by running:
npm -v

What should I see?

PowerShell should print an npm version number. The Node.js installer supplies both tools.

Still seeing the older version?

Close every PowerShell window before checking again. Windows refreshes the command path when a new terminal starts.

If the older version remains, rerun the matching installer. Choose the repair or update path presented by the installer.

Help me update Node.js on Windows You can also share the result in the NextWork community.

ⓧ Command not found

Node.js is not installed yet. The official Windows installer adds Node.js plus npm to your computer.

  • Check the System type value on the Windows system information page.
  • Download node-v24.20.0-x64.msi for an x64-based computer.
  • Download node-v24.20.0-arm64.msi for an ARM-based computer.
  • Run the downloaded installer.
  • Follow the installer prompts using the default options.
  • Close the existing PowerShell window.
  • Reopen Windows PowerShell through Windows Search.
  • Confirm the Node.js installation by running:
node -v

What should I see?

PowerShell should print v24.20.0. This proves that Windows can find the installed runtime.

  • Confirm that npm was installed by running:
npm -v

What should I see?

PowerShell should print an npm version number. You now have the package manager needed for the client dependencies.

Still cannot run Node.js?

Confirm that you reopened PowerShell after the installer finished. An older terminal does not receive the updated command path.

Confirm that the installer architecture matches the Windows system type.

Help me fix my Node.js installation You can also share the command result in the NextWork community.

NovaCart's development page needs a browser for its visible checks. Confirm that Microsoft Edge is available before you create the client.

  • Press the Windows key to open Windows Search.
  • Type Microsoft Edge into the search field.
  • Press Enter.

✔️ Microsoft Edge opens

Your browser is ready for the client check later in this step.

ⓧ Microsoft Edge is unavailable

Install Microsoft Edge before continuing so you can verify the client in the expected browser.

  • Visit the Microsoft Edge download page.
  • Select Download Edge for Windows.
  • Run the downloaded installer.
  • Open Microsoft Edge through Windows Search after installation.
Create the client workspace and configuration

A Vite client uses package.json to pin its packages. Its configuration file activates React support during development.

  • Press the Windows key to open Windows Search.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.
  • Select File from the top menu.
  • Select Open Folder.
  • Choose your Desktop as the location.

Where will NovaCart live?

The novacart folder holds the complete project. Its client folder keeps the browser application separate from the server you create later.

  • Create a folder named novacart on your Desktop.
  • Select the new novacart folder as the Visual Studio Code workspace.
  • Create a client folder inside novacart using the New Folder control in the Explorer sidebar.
  • Select the client folder in the Explorer sidebar.
  • Create package.json using the New File control.
  • Fill client/package.json by pasting this code:
{"name":"novacart-client","private":true,"version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"vite build","preview":"vite preview"},"dependencies":{"react":"19.2.8","react-dom":"19.2.8"},"devDependencies":{"@vitejs/plugin-react":"6.1.1","vite":"8.2.2"}}

What does this file do?

  • The package metadata identifies this private client project.
  • The scripts map development tasks to Vite commands.
  • The dependency sections pin React, React DOM, Vite, plus the React plugin to exact versions.
  • Save client/package.json with Ctrl+S.
  • Confirm that package.json appears inside client in the Explorer sidebar.

Is package.json showing an error?

Confirm that the pasted content begins with an opening brace. It must finish with a closing brace.

Help me check my package.json file

  • Select the client folder in the Explorer sidebar.
  • Create vite.config.js using the New File control.
  • Fill client/vite.config.js by pasting this code:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})

What does this configuration do?

  • The Vite helper checks the configuration structure.
  • The React plugin lets Vite process the client components.
  • The exported configuration activates that plugin when the development server starts.
  • Save client/vite.config.js with Ctrl+S.
  • Confirm that vite.config.js appears beside package.json in the Explorer sidebar.

Is the configuration underlined?

Confirm that the file is named vite.config.js. Check that both import lines match the code above.

Help me check my Vite configuration

  • Right-click the client folder in the Explorer sidebar.
  • Select Open in Integrated Terminal.
  • Install the pinned client dependencies by running:
npm install

What does this command do?

The npm install command reads package.json. It downloads the listed client dependencies into the local project.

  • Wait for the PowerShell prompt to return after the installation finishes.
  • Confirm that PowerShell shows a package installation summary.

Did the dependency installation fail?

Confirm that the terminal path ends with novacart\client. Check that package.json is saved before retrying the command.

Help me troubleshoot npm install You can also share the installation output in the NextWork community.

Build and run the React foundation

The browser begins with index.html. React then mounts the App component inside its root element.

  • Select the client folder in the Explorer sidebar.
  • Create index.html using the New File control.
  • Fill client/index.html by pasting this code:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="NovaCart MERN mini e-commerce store" />
    <title>NovaCart</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

What does this page do?

  • The document metadata gives the page its title plus mobile viewport settings.
  • The root element provides the mounting point for React.
  • The module script loads src/main.jsx as the client entry point.
  • Save client/index.html with Ctrl+S.
  • Confirm that index.html appears inside client in the Explorer sidebar.

Is index.html in the wrong folder?

Move index.html directly under client. It should sit beside package.json.

Help me place the NovaCart HTML file

  • Create a src folder inside client using the New Folder control.
  • Select the src folder in the Explorer sidebar.
  • Create main.jsx using the New File control.
  • Fill client/src/main.jsx by pasting this code:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './App.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

What does the entry point do?

  • The imports bring in Strict Mode, React's browser renderer, the app component, plus its stylesheet.
  • The root lookup targets the matching element in index.html.
  • The render call places the App component on the page.
  • Save client/src/main.jsx with Ctrl+S.
  • Confirm that main.jsx appears inside client/src in the Explorer sidebar.

Is main.jsx showing import warnings?

The component files referenced by this entry point are created next. Confirm that the current file is named main.jsx.

Help me check the React entry point

  • Select the src folder in the Explorer sidebar.
  • Create App.jsx using the New File control.
  • Fill client/src/App.jsx by pasting this code:
export default function App() {
  return (
    <main className="app-shell">
      <p className="eyebrow">MERN MINI STORE</p>
      <h1>NovaCart</h1>
      <p>Your store foundation is running.</p>
    </main>
  );
}

What does this component show?

  • The exported App function gives the entry point a component to render.
  • The page content identifies the project as NovaCart.
  • The foundation message provides a visible success signal in the browser.
  • Save client/src/App.jsx with Ctrl+S.
  • Confirm that App.jsx appears beside main.jsx in the Explorer sidebar.

Is App.jsx showing a syntax warning?

Confirm that the returned markup has one surrounding main element. Check that the function finishes with its closing brace.

Help me check the NovaCart App component

  • Select the src folder in the Explorer sidebar.
  • Create App.css using the New File control.
  • Fill client/src/App.css by pasting this code:
:root {
  font-family: Inter, ui-sans-serif, system-ui, sans-serif;
  color: #18212f;
  background: #eef2f7;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
}

.app-shell {
  padding: 2rem;
}

.eyebrow {
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.14em;
}

What does this styling change?

  • The root styles set the page typography plus its foreground and background colours.
  • The universal rule keeps element dimensions predictable.
  • The app shell adds breathing room around the foundation content.
  • The eyebrow class turns the store label into a compact heading detail.
  • Save client/src/App.css with Ctrl+S.
  • Confirm that App.css appears beside App.jsx in the Explorer sidebar.

Is the stylesheet underlined?

Confirm that every selector has matching braces. Check that the file is named App.css with the same capitalization used in main.jsx.

Help me check the NovaCart stylesheet

How the development server behaves

Vite keeps the integrated terminal busy while it serves the client. You can stop the server later by pressing Ctrl+C in that terminal.

Before you start the client, make your prediction about whether Edge will show the NovaCart heading or a blank page.

  • Start the Vite development server by running:
npm run dev

What does this command do?

The npm run dev command runs the dev script from package.json. Vite serves the client at its local development address.

  • Wait for PowerShell to display the local development address.
  • Switch back to Microsoft Edge.
  • Enter http://localhost:5173 in the address bar.
  • Press Enter to load NovaCart.

You should see MERN MINI STORE above the NovaCart heading. The page also shows the message Your store foundation is running.

Do you see a blank page?

Confirm that the PowerShell terminal remains open while you visit the local address. Check that the terminal path ends with novacart\client.

Compare the first error shown in the terminal with the matching file in Visual Studio Code. A filename or capitalization mismatch can stop the page from rendering.

Help me debug the NovaCart client

That is your first visible NovaCart win. The React client now renders through Vite in Microsoft Edge.

Use these tabs to confirm that every client file matches the working foundation.

✔️ Awesome, I've got everything!

Great work. Save every client file while the development server keeps running.

ⓧ I'd like to double check the full code

The complete client/package.json file should match this reference.

{"name":"novacart-client","private":true,"version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"vite build","preview":"vite preview"},"dependencies":{"react":"19.2.8","react-dom":"19.2.8"},"devDependencies":{"@vitejs/plugin-react":"6.1.1","vite":"8.2.2"}}

What should match?

Check the package name, scripts, dependency versions, plus development dependency versions against your saved file.

The complete client/vite.config.js file should match this reference.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})

What should match?

Check both imports plus the exported plugin configuration against your saved file.

The complete client/index.html file should match this reference.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="NovaCart MERN mini e-commerce store" />
    <title>NovaCart</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

What should match?

Check the metadata, root element, plus the module script path against your saved file.

The complete client/src/main.jsx file should match this reference.

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './App.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

What should match?

Check all four imports plus the complete render call against your saved entry point.

The complete client/src/App.jsx file should match this reference.

export default function App() {
  return (
    <main className="app-shell">
      <p className="eyebrow">MERN MINI STORE</p>
      <h1>NovaCart</h1>
      <p>Your store foundation is running.</p>
    </main>
  );
}

What should match?

Check the exported component plus all three visible text elements against your saved file.

The complete client/src/App.css file should match this reference.

:root {
  font-family: Inter, ui-sans-serif, system-ui, sans-serif;
  color: #18212f;
  background: #eef2f7;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
}

.app-shell {
  padding: 2rem;
}

.eyebrow {
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.14em;
}

What should match?

Check all four selectors plus their declarations against your saved stylesheet.

Your browser foundation is ready. Next, you will connect NovaCart to a MongoDB Atlas database.

Create the Atlas Database

Your NovaCart client is running in Microsoft Edge. Its future products and orders still need a durable home.

MongoDB Atlas provides that managed database. In this step, you will create a Free cluster. A small Node.js server will prove that it can reach the cluster without placing the connection string in source code.

In this step, get ready to:
  • Create a Free MongoDB Atlas cluster.
  • Protect the cluster with a database user and a current-IP access-list entry.
  • Build a server connection test and verify the Atlas connection.
Create a Free Atlas cluster

An Atlas cluster stores your database collections. The Free option is priced at $0/hour. It also requires no credit card.

  • In Microsoft Edge, open MongoDB's Free cluster guide.
  • Create a free MongoDB Atlas account from the guide.
  • Sign in to MongoDB Atlas.
  • Create a new Atlas project for this store.
  • Open Clusters in the Atlas project.
  • Choose Build a Cluster.
  • Select Free clusters.

Why Use a Free Cluster?

A Free cluster gives NovaCart a managed database without local database installation. Free clusters never expire.

Atlas allows one Free cluster in each project. That is enough for the complete product and order workflow.

Atlas may spend a few minutes provisioning the cluster. The building status means Atlas is preparing your database.

  • Confirm the cluster configuration to start provisioning.
  • Wait until Atlas shows that the cluster is ready.
Secure the Atlas connection

Atlas protects the cluster with a database user and an IP access list. Both checks must allow your server before Atlas accepts a connection.

The database password grants access to your data. It stays out of screenshots and project source code throughout this project.

  • Select Connect on your cluster.
  • Choose Standard Connection.
  • Select Add Your Current IP Address.
  • Enter a database username.
  • Generate a strong database password.
  • Save the password in your password manager.
  • Select Create Database User.

That is the sensitive part complete. Your cluster now has one credential for NovaCart and one approved network location.

Why Limit Access to Your Current IP?

The IP access list controls which network locations can contact the cluster. Adding only your current IP keeps the connection boundary focused on the computer you are using.

A future network change can give your computer a different public IP address. Atlas lets you add the new current address through the same connection flow.

  • Continue to Choose Your Connection Method.
  • Select Drivers.
  • Select Node.js as the driver.
  • Copy the SRV connection string that begins with mongodb+srv://.
  • Replace <db_password> with the database password you saved.
  • Keep the Atlas connection tab available with the completed connection string.
Build the connection test

An environment variable gives the server access to the connection string at runtime. A .gitignore rule keeps the credential file outside version control.

  • Switch back to Visual Studio Code from earlier.
  • Create a server folder inside the open novacart workspace using the Explorer sidebar.
  • Create package.json inside novacart/server using the Explorer sidebar.
  • Add the server scripts and dependencies by pasting this code into package.json:
{"name":"novacart-server","private":true,"version":"1.0.0","main":"index.js","scripts":{"start":"node index.js"},"dependencies":{"cors":"2.8.6","express":"5.2.1","mongodb":"7.6.0"}}

What Does This Configuration Do?

  • The start script runs index.js with Node.js.
  • Express provides the API framework used later in the project.
  • cors allows the browser client to call the server later in the project.
  • MongoDB Node.js Driver provides MongoClient for the Atlas connection.
  • Save server/package.json.
  • Use the terminal controls in Visual Studio Code to create a second Windows PowerShell terminal at the novacart workspace.
  • Move the second terminal into server by running this command:
Set-Location -Path server
  • Install the server dependencies by running this command:
npm install

PowerShell finishes with an installation summary. You will also see a node_modules folder under server.

Dependency Installation Failed?

Confirm that the terminal path ends with novacart\server. Check that server/package.json contains valid JSON.

If PowerShell reports that npm is unavailable, close the terminal and create a new one so it can use the installed Node.js tools.

Help me troubleshoot the NovaCart server dependency installation

The connection string is a credential. The next files separate that credential from the JavaScript connection test.

  • Create .gitignore inside novacart/server using the Explorer sidebar.
  • Add the ignored server files by pasting this code into .gitignore:
node_modules/
.env

What Does This File Protect?

The node_modules/ rule excludes installed packages. The .env rule excludes the file that holds your Atlas connection string.

  • Save server/.gitignore.
  • Confirm that .gitignore appears inside the server folder in the Explorer sidebar.
  • Create .env inside novacart/server using the Explorer sidebar.
  • Add the redacted connection-string template by pasting this line into .env:
MONGODB_URI=your-mongodb-atlas-connection-string-here

What Does This Setting Do?

MONGODB_URI is the environment variable name read by the server. The placeholder keeps the secret out of the displayed project code.

Your saved .env file receives the completed SRV connection string from Atlas.

  • Replace your-mongodb-atlas-connection-string-here with the completed SRV connection string from your Atlas tab.
  • Save server/.env.
  • Confirm that .env appears inside the server folder without sharing its contents.

The connection test loads the protected value before creating a MongoDB client. Its ping command asks the Atlas deployment to confirm that the server can reach it.

  • Create index.js inside novacart/server using the Explorer sidebar.
  • Add the Atlas connection test by pasting this code into index.js:
const { loadEnvFile } = require('node:process');
const { MongoClient, ServerApiVersion } = require('mongodb');

loadEnvFile();

const client = new MongoClient(process.env.MONGODB_URI, {
  serverApi: { version: ServerApiVersion.v1, strict: true, deprecationErrors: true },
});

async function testConnection() {
  await client.connect();
  await client.db('admin').command({ ping: 1 });
  console.log('Pinged your deployment. You successfully connected to MongoDB!');
  await client.close();
}

testConnection().catch((error) => {
  console.error('Could not connect to MongoDB:', error.message);
  process.exitCode = 1;
});

What Does This Code Do?

  • loadEnvFile() loads server/.env into process.env.
  • MongoClient uses MONGODB_URI to configure the Atlas connection.
  • testConnection() connects to Atlas and sends the admin database a ping command.
  • client.close() closes the test connection after the successful response.
  • catch() prints a useful message when the connection cannot be established.
  • Save server/index.js.

Before you run the test, do you think Atlas will accept this server from your current network?

  • Test the Atlas connection from the server terminal by running this command:
npm start

PowerShell prints Pinged your deployment. You successfully connected to MongoDB!.

You have cleared the connection hurdle. NovaCart now has a protected path from its server folder to the Atlas database.

Connection Test Failed?

  • Check that .env is saved inside novacart/server.
  • Confirm that the environment variable name is exactly MONGODB_URI.
  • Return to Atlas to add your current IP address again if your network changed.

Help me diagnose my MongoDB Atlas connection test

✔️ Awesome, I've got everything!

Your four server files are in place. Your real Atlas connection string stays only inside server/.env.

ⓧ I'd like to double check the full code

The .env reference stays redacted below. Your saved file contains your completed Atlas connection string.

{"name":"novacart-server","private":true,"version":"1.0.0","main":"index.js","scripts":{"start":"node index.js"},"dependencies":{"cors":"2.8.6","express":"5.2.1","mongodb":"7.6.0"}}
MONGODB_URI=your-mongodb-atlas-connection-string-here
node_modules/
.env
const { loadEnvFile } = require('node:process');
const { MongoClient, ServerApiVersion } = require('mongodb');

loadEnvFile();

const client = new MongoClient(process.env.MONGODB_URI, {
  serverApi: { version: ServerApiVersion.v1, strict: true, deprecationErrors: true },
});

async function testConnection() {
  await client.connect();
  await client.db('admin').command({ ping: 1 });
  console.log('Pinged your deployment. You successfully connected to MongoDB!');
  await client.close();
}

testConnection().catch((error) => {
  console.error('Could not connect to MongoDB:', error.message);
  process.exitCode = 1;
});

Your database connection is live. Next, you will turn the running client foundation into a storefront with a local shopping cart.

Build the Storefront and Local Cart

Your secure MongoDB Atlas connection is ready. The next piece is the shopper-facing experience in Microsoft Edge.

React component state can hold a cart while the shopper uses the page. This creates a complete local shopping interaction before the API joins the flow.

In this step, get ready to:
  • Render four starter products as reusable cards.
  • Manage cart quantities through immutable state updates.
  • Style the storefront and test its browser-only behavior.
Build the in-memory storefront

The storefront needs product data before it can render reusable cards. Its cart also needs one source of truth for every quantity and total.

  • In the Explorer sidebar of Visual Studio Code, select client/src/App.jsx.
  • Select all existing code in client/src/App.jsx.
  • Build the local catalog and cart by replacing the selected code with:
import { useState } from 'react';

const starterProducts = [
  { _id: 'backpack', name: 'Everyday Backpack', category: 'Travel', description: 'A compact carry-all for work, study, and short trips.', price: 49.99, badge: 'B' },
  { _id: 'lamp', name: 'Desk Lamp', category: 'Home', description: 'A clean reading light for a focused workspace.', price: 34.5, badge: 'L' },
  { _id: 'keyboard', name: 'Wireless Keyboard', category: 'Technology', description: 'A low-profile keyboard for a tidy desktop setup.', price: 59, badge: 'K' },
  { _id: 'mug', name: 'Ceramic Mug', category: 'Kitchen', description: 'A durable mug for coffee, tea, or a desk-side break.', price: 16.75, badge: 'M' },
];
const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
function formatCurrency(value) { return currency.format(value); }
export default function App() {
  const [cart, setCart] = useState([]);
  function addToCart(product) { setCart((current) => { const existing = current.find((item) => item._id === product._id); return existing ? current.map((item) => item._id === product._id ? { ...item, quantity: item.quantity + 1 } : item) : [...current, { ...product, quantity: 1 }]; }); }
  function changeQuantity(productId, amount) { setCart((current) => current.map((item) => item._id === productId ? { ...item, quantity: item.quantity + amount } : item).filter((item) => item.quantity > 0)); }
  function removeFromCart(productId) { setCart((current) => current.filter((item) => item._id !== productId)); }
  const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0);
  const cartTotal = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  return <div className="app-shell"><header className="site-header"><h1>NovaCart</h1><div className="cart-count">Cart {cartCount}</div></header><main className="store-layout"><section className="catalog"><h2>Everyday essentials</h2><div className="product-grid">{starterProducts.map((product) => <article className="product-card" key={product._id}><div className="product-badge">{product.badge}</div><p className="category">{product.category}</p><h3>{product.name}</h3><p className="description">{product.description}</p><div className="product-footer"><strong>{formatCurrency(product.price)}</strong><button type="button" onClick={() => addToCart(product)}>Add to cart</button></div></article>)}</div></section><aside className="cart-panel"><h2>Shopping cart</h2>{cart.length === 0 ? <p className="empty-cart">Your cart is empty.</p> : <div className="cart-list">{cart.map((item) => <div className="cart-row" key={item._id}><strong>{item.name}</strong><div className="quantity-controls"><button type="button" onClick={() => changeQuantity(item._id, -1)}>-</button><span>{item.quantity}</span><button type="button" onClick={() => changeQuantity(item._id, 1)}>+</button><button className="remove-button" type="button" onClick={() => removeFromCart(item._id)}>Remove</button></div></div>)}</div>}<div className="cart-total"><span>Total</span><strong>{formatCurrency(cartTotal)}</strong></div></aside></main></div>;
}

How does the local cart work?

  • The starterProducts array supplies the four products displayed in the catalog.
  • The cart state stores each selected product with its current quantity.
  • The addToCart() function adds a product or creates a new array with its quantity increased.
  • The changeQuantity() function adjusts a quantity before filtering out rows that reach zero.
  • The removeFromCart() function creates an array without the selected product.
  • The cartCount and cartTotal calculations derive the displayed values from the current cart.
  • Save client/src/App.jsx.
  • Switch back to the Microsoft Edge tab from earlier.

You should see four product cards beside an empty shopping cart. The cart count should display Cart 0.

Do the product cards fail to appear?

Check that useState is imported at the top of client/src/App.jsx. Confirm that every opening bracket has a matching closing bracket.

Ask for help with the component if the browser shows an error instead of the catalog.

✔️ Awesome, I've got everything!

Your local storefront logic is in place. Make sure client/src/App.jsx is saved before continuing.

ⓧ I'd like to double check the full code

import { useState } from 'react';

const starterProducts = [
  { _id: 'backpack', name: 'Everyday Backpack', category: 'Travel', description: 'A compact carry-all for work, study, and short trips.', price: 49.99, badge: 'B' },
  { _id: 'lamp', name: 'Desk Lamp', category: 'Home', description: 'A clean reading light for a focused workspace.', price: 34.5, badge: 'L' },
  { _id: 'keyboard', name: 'Wireless Keyboard', category: 'Technology', description: 'A low-profile keyboard for a tidy desktop setup.', price: 59, badge: 'K' },
  { _id: 'mug', name: 'Ceramic Mug', category: 'Kitchen', description: 'A durable mug for coffee, tea, or a desk-side break.', price: 16.75, badge: 'M' },
];
const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
function formatCurrency(value) { return currency.format(value); }
export default function App() {
  const [cart, setCart] = useState([]);
  function addToCart(product) { setCart((current) => { const existing = current.find((item) => item._id === product._id); return existing ? current.map((item) => item._id === product._id ? { ...item, quantity: item.quantity + 1 } : item) : [...current, { ...product, quantity: 1 }]; }); }
  function changeQuantity(productId, amount) { setCart((current) => current.map((item) => item._id === productId ? { ...item, quantity: item.quantity + amount } : item).filter((item) => item.quantity > 0)); }
  function removeFromCart(productId) { setCart((current) => current.filter((item) => item._id !== productId)); }
  const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0);
  const cartTotal = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  return <div className="app-shell"><header className="site-header"><h1>NovaCart</h1><div className="cart-count">Cart {cartCount}</div></header><main className="store-layout"><section className="catalog"><h2>Everyday essentials</h2><div className="product-grid">{starterProducts.map((product) => <article className="product-card" key={product._id}><div className="product-badge">{product.badge}</div><p className="category">{product.category}</p><h3>{product.name}</h3><p className="description">{product.description}</p><div className="product-footer"><strong>{formatCurrency(product.price)}</strong><button type="button" onClick={() => addToCart(product)}>Add to cart</button></div></article>)}</div></section><aside className="cart-panel"><h2>Shopping cart</h2>{cart.length === 0 ? <p className="empty-cart">Your cart is empty.</p> : <div className="cart-list">{cart.map((item) => <div className="cart-row" key={item._id}><strong>{item.name}</strong><div className="quantity-controls"><button type="button" onClick={() => changeQuantity(item._id, -1)}>-</button><span>{item.quantity}</span><button type="button" onClick={() => changeQuantity(item._id, 1)}>+</button><button className="remove-button" type="button" onClick={() => removeFromCart(item._id)}>Remove</button></div></div>)}</div>}<div className="cart-total"><span>Total</span><strong>{formatCurrency(cartTotal)}</strong></div></aside></main></div>;
}
Test immutable cart updates

Each cart action derives a new array from the current state. React then renders the cart from that new array.

  • Select Add to cart on Everyday Backpack.
  • Select Add to cart on Desk Lamp.

You should see both products in the cart. The header count should display Cart 2.

  • Select the + control beside Everyday Backpack.

The backpack quantity should become 2. The header count should become Cart 3.

  • Select the - control beside Everyday Backpack.
  • Select Remove beside Desk Lamp.

You should see one Everyday Backpack in the cart. Its price should also become the cart total.

Why do immutable updates matter?

The spread syntax in addToCart() creates a new cart row when needed. The map() method creates updated rows without changing the originals.

The filter() method returns a new array after removing unwanted rows. New array references help React recognize each cart change.

Style and verify the local store

The shopping behavior now works. The stylesheet turns that raw structure into a responsive catalog with a distinct cart panel.

  • In the Explorer sidebar, select client/src/App.css.
  • Select all existing content in client/src/App.css.
  • Create the page foundation and header by replacing the selected content with:
:root {
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
    "Segoe UI", sans-serif;
  color: #18212f;
  background: #eef2f7;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; min-height: 100vh; }
button,
input { font: inherit; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; opacity: 0.55; }
.app-shell { min-height: 100vh; }
.site-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1.25rem clamp(1rem, 4vw, 4rem); color: white; background: #172554; }
.site-header h1,
.section-heading h2,
.cart-panel h2 { margin: 0; }
.site-header h1 { font-size: clamp(1.8rem, 4vw, 2.6rem); }
.eyebrow { margin: 0 0 0.35rem; font-size: 0.72rem; font-weight: 800; letter-spacing: 0.14em; }
.cart-count { padding: 0.6rem 0.9rem; border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 999px; font-weight: 700; }

What do the foundation styles do?

  • The root rules establish the page font and the light background.
  • The shared box sizing keeps padding inside each element's measured width.
  • The header rules create a dark navigation band with a pill-shaped cart count.
  • Save client/src/App.css.
  • Switch back to Microsoft Edge.

You should see NovaCart inside a dark blue header. The cart count should appear inside a rounded outline.

Is the header still unstyled?

Confirm that main.jsx still imports ./App.css. Check that the stylesheet is saved inside client/src.

Ask for help if the page keeps using the old foundation styles.

  • Add the catalog layout rules by pasting this code at the end of client/src/App.css:
.store-layout { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 1.5rem; width: min(1180px, calc(100% - 2rem)); margin: 2rem auto; align-items: start; }
.catalog,
.cart-panel { padding: 1.25rem; border: 1px solid #d9e0ea; border-radius: 18px; background: white; box-shadow: 0 12px 30px rgba(23, 37, 84, 0.08); }
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; }
.section-heading > p { margin: 0; color: #64748b; }
input[type="search"] { width: min(300px, 100%); padding: 0.7rem 0.85rem; border: 1px solid #cbd5e1; border-radius: 10px; }
.product-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.product-card { display: flex; flex-direction: column; min-height: 275px; padding: 1rem; border: 1px solid #e2e8f0; border-radius: 14px; }
.product-badge { display: grid; width: 64px; height: 64px; place-items: center; border-radius: 16px; color: #172554; background: #dbeafe; font-size: 1.5rem; font-weight: 900; }
.category { margin: 1rem 0 0.25rem; color: #2563eb; font-size: 0.75rem; font-weight: 800; text-transform: uppercase; }
.product-card h3 { margin: 0; }
.description { flex: 1; color: #64748b; line-height: 1.5; }
.product-footer { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.product-footer button,
.checkout-button { border: 0; border-radius: 10px; color: white; background: #2563eb; font-weight: 800; }
.product-footer button { padding: 0.65rem 0.8rem; }

What do the catalog styles do?

  • The store layout reserves a wide area for products and a narrower area for the cart.
  • The product grid displays two cards per row on wider screens.
  • Each product card uses flexible spacing to keep its price and button aligned.
  • Save client/src/App.css.
  • Return to Microsoft Edge.

You should see two product cards per row. Each card should have a blue badge and an aligned Add to cart button.

Are the products still in one plain column?

Check that product-grid matches the class used by the product container. Confirm that the closing brace for store-layout is present.

Ask for help comparing the catalog selectors with the JSX classes.

  • Add the cart presentation rules by pasting this code at the end of client/src/App.css:
.cart-panel { position: sticky; top: 1rem; }
.empty-cart,
.status { color: #64748b; }
.cart-list { display: grid; gap: 0.85rem; margin: 1rem 0; }
.cart-row { padding-bottom: 0.85rem; border-bottom: 1px solid #e2e8f0; }
.cart-row > div:first-child { display: flex; justify-content: space-between; gap: 0.5rem; }
.cart-row span { color: #64748b; font-size: 0.9rem; }
.quantity-controls { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.65rem; }
.quantity-controls button { min-width: 32px; padding: 0.35rem 0.55rem; border: 1px solid #cbd5e1; border-radius: 8px; background: white; }
.quantity-controls .remove-button { margin-left: auto; color: #b91c1c; }
.cart-total { display: flex; justify-content: space-between; padding: 1rem 0; border-top: 2px solid #172554; font-size: 1.1rem; }
.checkout-button { width: 100%; padding: 0.85rem; }
.notice { padding: 0.75rem; border-radius: 10px; color: #1e3a8a; background: #dbeafe; overflow-wrap: anywhere; }

What do the cart styles do?

  • The cart panel remains visible near the top while the shopper scrolls on a wide screen.
  • The quantity controls use compact buttons with consistent spacing.
  • The total receives a strong top border so it stays separate from the cart rows.
  • Save client/src/App.css.
  • Return to Microsoft Edge.

You should see a raised white cart panel with compact quantity controls. The total should sit below a dark divider.

Do the quantity controls look unstyled?

Confirm that the stylesheet uses .quantity-controls with the leading period. Check that the JSX uses quantity-controls with the same spelling.

Ask for help if only the cart controls ignore the stylesheet.

  • Add the responsive rules by pasting this code at the end of client/src/App.css:
@media (max-width: 820px) {
  .store-layout {
    grid-template-columns: 1fr;
  }

  .cart-panel {
    position: static;
  }
}

@media (max-width: 560px) {
  .product-grid {
    grid-template-columns: 1fr;
  }

  .section-heading {
    align-items: start;
    flex-direction: column;
  }
}

What do the responsive rules do?

The first breakpoint moves the cart below the catalog when the window becomes narrow. The second breakpoint displays one product card per row on small screens.

  • Save client/src/App.css.
  • Narrow the Microsoft Edge window until the cart moves below the catalog.

You should see the two-column layout become one column. At the narrowest width, each product should occupy its own row.

Does the layout stay in two columns?

Confirm that both media queries include their closing braces. Check that the viewport metadata remains present in client/index.html.

Ask for help if changing the browser width does not rearrange the layout.

✔️ Awesome, I've got everything!

Your responsive storefront styles are complete. Make sure client/src/App.css is saved.

ⓧ I'd like to double check the full code

:root {
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
    "Segoe UI", sans-serif;
  color: #18212f;
  background: #eef2f7;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

button,
input {
  font: inherit;
}

button {
  cursor: pointer;
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

.app-shell {
  min-height: 100vh;
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.25rem clamp(1rem, 4vw, 4rem);
  color: white;
  background: #172554;
}

.site-header h1,
.section-heading h2,
.cart-panel h2 {
  margin: 0;
}

.site-header h1 {
  font-size: clamp(1.8rem, 4vw, 2.6rem);
}

.eyebrow {
  margin: 0 0 0.35rem;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.14em;
}

.cart-count {
  padding: 0.6rem 0.9rem;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 999px;
  font-weight: 700;
}

.store-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 1.5rem;
  width: min(1180px, calc(100% - 2rem));
  margin: 2rem auto;
  align-items: start;
}

.catalog,
.cart-panel {
  padding: 1.25rem;
  border: 1px solid #d9e0ea;
  border-radius: 18px;
  background: white;
  box-shadow: 0 12px 30px rgba(23, 37, 84, 0.08);
}

.section-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
}

.section-heading > p {
  margin: 0;
  color: #64748b;
}

input[type="search"] {
  width: min(300px, 100%);
  padding: 0.7rem 0.85rem;
  border: 1px solid #cbd5e1;
  border-radius: 10px;
}

.product-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}

.product-card {
  display: flex;
  flex-direction: column;
  min-height: 275px;
  padding: 1rem;
  border: 1px solid #e2e8f0;
  border-radius: 14px;
}

.product-badge {
  display: grid;
  width: 64px;
  height: 64px;
  place-items: center;
  border-radius: 16px;
  color: #172554;
  background: #dbeafe;
  font-size: 1.5rem;
  font-weight: 900;
}

.category {
  margin: 1rem 0 0.25rem;
  color: #2563eb;
  font-size: 0.75rem;
  font-weight: 800;
  text-transform: uppercase;
}

.product-card h3 {
  margin: 0;
}

.description {
  flex: 1;
  color: #64748b;
  line-height: 1.5;
}

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

.product-footer button,
.checkout-button {
  border: 0;
  border-radius: 10px;
  color: white;
  background: #2563eb;
  font-weight: 800;
}

.product-footer button {
  padding: 0.65rem 0.8rem;
}

.cart-panel {
  position: sticky;
  top: 1rem;
}

.empty-cart,
.status {
  color: #64748b;
}

.cart-list {
  display: grid;
  gap: 0.85rem;
  margin: 1rem 0;
}

.cart-row {
  padding-bottom: 0.85rem;
  border-bottom: 1px solid #e2e8f0;
}

.cart-row > div:first-child {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
}

.cart-row span {
  color: #64748b;
  font-size: 0.9rem;
}

.quantity-controls {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.65rem;
}

.quantity-controls button {
  min-width: 32px;
  padding: 0.35rem 0.55rem;
  border: 1px solid #cbd5e1;
  border-radius: 8px;
  background: white;
}

.quantity-controls .remove-button {
  margin-left: auto;
  color: #b91c1c;
}

.cart-total {
  display: flex;
  justify-content: space-between;
  padding: 1rem 0;
  border-top: 2px solid #172554;
  font-size: 1.1rem;
}

.checkout-button {
  width: 100%;
  padding: 0.85rem;
}

.notice {
  padding: 0.75rem;
  border-radius: 10px;
  color: #1e3a8a;
  background: #dbeafe;
  overflow-wrap: anywhere;
}

@media (max-width: 820px) {
  .store-layout {
    grid-template-columns: 1fr;
  }

  .cart-panel {
    position: static;
  }
}

@media (max-width: 560px) {
  .product-grid {
    grid-template-columns: 1fr;
  }

  .section-heading {
    align-items: start;
    flex-direction: column;
  }
}

The final check starts the client from its script and proves the local cart responds to a complete shopping interaction.

  • Return to the client Windows PowerShell terminal from earlier.
  • Start Vite by running:
npm run dev

What does this command do?

The command executes the dev script from client/package.json. Vite then serves the storefront locally.

PowerShell should display http://localhost:5173 as the local address.

Does the development server fail to start?

Confirm that the terminal is inside the client folder. Check that the dependencies from the earlier installation remain available.

Ask for help if the client script does not display the local address.

  • Switch back to Microsoft Edge.
  • Visit http://localhost:5173.
  • Select Add to cart on Everyday Backpack.
  • Select Add to cart on Desk Lamp.
  • Select the + control beside Desk Lamp.

You should see both products in the styled cart. Desk Lamp should have a quantity of 2.

That is the local shopping loop working. NovaCart now recalculates its cart count and total after every quantity change.

Before you refresh, consider whether the cart count and total stay visible after the page reloads.

  • Reload the page in Microsoft Edge.

After the reload, you should see Cart 0, $0.00, and Your cart is empty..

That intended reset proves the cart exists only in browser memory. NovaCart also has no order confirmation because the client has no API connection.

Your storefront can now handle a complete local cart interaction. Next, you will connect it to the MERN backend so products and orders can move beyond browser memory.

Connect the MERN Backend

Your local NovaCart storefront can already manage a cart in browser memory. The missing half of the MERN stack is a server that can load products from MongoDB Atlas and save each checkout.

In this step, you will turn the connection test into an Express API. You will also connect the React client to that API so refreshing the page no longer limits the store to hardcoded products.

In this step, get ready to:
  • Replace the database connection test with the NovaCart API.
  • Load products into the client through a REST endpoint.
  • Submit a simulated order with a server-calculated total.
Turn the connection test into an API

The browser should send product IDs plus quantities during checkout. The server can then reload trusted prices from the database before it calculates the total.

  • Return to the novacart folder in the VS Code Explorer sidebar.
  • Select server/index.js.
  • Replace its contents with the complete API shown in the double-check tab below:

✔️ Awesome, I've got everything!

Your server file now contains the database connection, API routes, validation logic, product seed data, and startup function.

ⓧ I'd like to double check the full code

const { loadEnvFile } = require('node:process');
const express = require('express');
var cors = require('cors');
const { MongoClient, ServerApiVersion } = require("mongodb");

loadEnvFile();

const app = express();
const PORT = 5050;
const CLIENT_ORIGIN = 'http://localhost:5173';

app.use(cors({ origin: CLIENT_ORIGIN }));
app.use(express.json());

const client = new MongoClient(process.env.MONGODB_URI, {
  serverApi: {
    version: ServerApiVersion.v1,
    strict: true,
    deprecationErrors: true,
  },
});

const starterProducts = [
  {
    _id: 'backpack',
    name: 'Everyday Backpack',
    category: 'Travel',
    description: 'A compact carry-all for work, study, and short trips.',
    price: 49.99,
    badge: 'B',
  },
  {
    _id: 'lamp',
    name: 'Desk Lamp',
    category: 'Home',
    description: 'A clean reading light for a focused workspace.',
    price: 34.5,
    badge: 'L',
  },
  {
    _id: 'keyboard',
    name: 'Wireless Keyboard',
    category: 'Technology',
    description: 'A low-profile keyboard for a tidy desktop setup.',
    price: 59,
    badge: 'K',
  },
  {
    _id: 'mug',
    name: 'Ceramic Mug',
    category: 'Kitchen',
    description: 'A durable mug for coffee, tea, or a desk-side break.',
    price: 16.75,
    badge: 'M',
  },
];

let products;
let orders;

app.get('/api/health', async (req, res) => {
  try {
    await client.db('admin').command({ ping: 1 });
    res.json({ status: 'ok' });
  } catch (error) {
    res.status(500).json({ error: 'Database connection failed.' });
  }
});

app.get('/api/products', async (req, res) => {
  try {
    const items = await products.find({}).toArray();
    res.json(items);
  } catch (error) {
    res.status(500).json({ error: 'Could not load products.' });
  }
});

app.post('/api/orders', async (req, res) => {
  try {
    const cartItems = req.body.items;
    const validItems =
      Array.isArray(cartItems) &&
      cartItems.length > 0 &&
      cartItems.every(
        (item) =>
          typeof item.productId === 'string' &&
          Number.isInteger(item.quantity) &&
          item.quantity > 0,
      );

    if (!validItems) {
      return res.status(400).json({ error: 'Order items are invalid.' });
    }

    const productIds = cartItems.map((item) => item.productId);
    if (new Set(productIds).size !== productIds.length) {
      return res.status(400).json({ error: 'Duplicate products are invalid.' });
    }

    const storedProducts = await products
      .find({ _id: { $in: productIds } })
      .toArray();

    if (storedProducts.length !== productIds.length) {
      return res.status(400).json({ error: 'One or more products do not exist.' });
    }

    const orderItems = cartItems.map((item) => {
      const product = storedProducts.find(
        (storedProduct) => storedProduct._id === item.productId,
      );
      return {
        productId: product._id,
        name: product.name,
        price: product.price,
        quantity: item.quantity,
      };
    });

    const total = Number(
      orderItems
        .reduce((sum, item) => sum + item.price * item.quantity, 0)
        .toFixed(2),
    );
    const orderNumber = `NC-${Date.now()}`;

    await orders.insertOne({
      orderNumber,
      items: orderItems,
      total,
      status: 'confirmed',
      createdAt: new Date(),
    });

    res.status(201).json({ orderNumber, total });
  } catch (error) {
    res.status(500).json({ error: 'Could not save the order.' });
  }
});

async function startServer() {
  await client.connect();
  await client.db('admin').command({ ping: 1 });

  const database = client.db('novacart');
  products = database.collection('products');
  orders = database.collection('orders');

  if ((await products.countDocuments({})) === 0) {
    await products.insertMany(starterProducts);
  }

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

startServer().catch((error) => {
  console.error('Could not start NovaCart:', error.message);
  process.exitCode = 1;
});

What does this server do?

  • The startup function connects to Atlas before it creates handles for the products collection and the orders collection.
  • The seed check inserts the four starter products only when the product collection is empty.
  • The product route returns database records as JSON.
  • The order route validates the cart before it embeds trusted product details in a confirmed order.
  • Save server/index.js.
  • Switch back to the integrated PowerShell terminal you used for the server connection test.
  • Start the NovaCart server by running:
npm start

What does this command start?

The start script runs server/index.js. The server loads MONGODB_URI from server/.env before it pings Atlas.

Successful startup also enables CORS for the client address. This lets the browser call the API running on a different local port.

PowerShell prints NovaCart API listening on http://localhost:5050. Your database now has collection handles ready for product reads and order writes.

Server not starting?

Confirm that the terminal prompt points to the server folder. Check that server/.env still contains your working Atlas connection string.

If Atlas rejects the connection, confirm that your current IP address remains in the project access list.

Help me diagnose why the NovaCart Express server cannot connect to MongoDB Atlas.

Connect the storefront to the API

The client currently renders its own product array. A data-loading effect moves that responsibility to the API while preserving the cart controls you already built.

  • Return to the VS Code Explorer sidebar.
  • Select client/src/App.jsx.
  • Replace its contents with the connected client shown in the double-check tab below:

✔️ Awesome, I've got everything!

Your client now loads products from the API and submits cart items through the checkout route.

ⓧ I'd like to double check the full code

import { useEffect, useState } from 'react';

const API_URL = 'http://localhost:5050/api';
const currency = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
});

function formatCurrency(value) {
  return currency.format(value);
}

export default function App() {
  const [products, setProducts] = useState([]);
  const [cart, setCart] = useState([]);
  const [loading, setLoading] = useState(true);
  const [notice, setNotice] = useState('');
  const [checkoutBusy, setCheckoutBusy] = useState(false);

  useEffect(() => {
    let ignore = false;

    async function loadProducts() {
      try {
        const response = await fetch(`${API_URL}/products`);
        if (!response.ok) {
          throw new Error(`Product request failed with ${response.status}`);
        }
        const data = await response.json();
        if (!ignore) {
          setProducts(data);
        }
      } catch (error) {
        if (!ignore) {
          setNotice(error.message);
        }
      } finally {
        if (!ignore) {
          setLoading(false);
        }
      }
    }

    loadProducts();
    return () => {
      ignore = true;
    };
  }, []);

  function addToCart(product) {
    setNotice('');
    setCart((current) => {
      const existing = current.find((item) => item._id === product._id);
      if (existing) {
        return current.map((item) =>
          item._id === product._id
            ? { ...item, quantity: item.quantity + 1 }
            : item,
        );
      }
      return [...current, { ...product, quantity: 1 }];
    });
  }

  function changeQuantity(productId, amount) {
    setCart((current) =>
      current
        .map((item) =>
          item._id === productId
            ? { ...item, quantity: item.quantity + amount }
            : item,
        )
        .filter((item) => item.quantity > 0),
    );
  }

  function removeFromCart(productId) {
    setCart((current) => current.filter((item) => item._id !== productId));
  }

  async function placeOrder() {
    if (cart.length === 0) {
      return;
    }

    setCheckoutBusy(true);
    setNotice('');

    try {
      const response = await fetch(`${API_URL}/orders`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          items: cart.map((item) => ({
            productId: item._id,
            quantity: item.quantity,
          })),
        }),
      });

      const data = await response.json();
      if (!response.ok) {
        throw new Error(data.error || 'Checkout failed.');
      }

      setCart([]);
      setNotice(
        `Order ${data.orderNumber} saved. Total: ${formatCurrency(data.total)}.`,
      );
    } catch (error) {
      setNotice(error.message);
    } finally {
      setCheckoutBusy(false);
    }
  }

  const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0);
  const cartTotal = cart.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0,
  );

  return (
    <div className="app-shell">
      <header className="site-header">
        <div>
          <p className="eyebrow">MERN MINI STORE</p>
          <h1>NovaCart</h1>
        </div>
        <div className="cart-count">Cart {cartCount}</div>
      </header>

      <main className="store-layout">
        <section className="catalog" aria-labelledby="catalog-title">
          <div className="section-heading">
            <div>
              <p className="eyebrow">CATALOG</p>
              <h2 id="catalog-title">Everyday essentials</h2>
            </div>
            <p>{products.length} products</p>
          </div>

          {loading ? (
            <p className="status">Loading products...</p>
          ) : (
            <div className="product-grid">
              {products.map((product) => (
                <article className="product-card" key={product._id}>
                  <div className="product-badge" aria-hidden="true">
                    {product.badge}
                  </div>
                  <p className="category">{product.category}</p>
                  <h3>{product.name}</h3>
                  <p className="description">{product.description}</p>
                  <div className="product-footer">
                    <strong>{formatCurrency(product.price)}</strong>
                    <button type="button" onClick={() => addToCart(product)}>
                      Add to cart
                    </button>
                  </div>
                </article>
              ))}
            </div>
          )}
        </section>

        <aside className="cart-panel" aria-labelledby="cart-title">
          <p className="eyebrow">YOUR ORDER</p>
          <h2 id="cart-title">Shopping cart</h2>

          {cart.length === 0 ? (
            <p className="empty-cart">Your cart is empty.</p>
          ) : (
            <div className="cart-list">
              {cart.map((item) => (
                <div className="cart-row" key={item._id}>
                  <div>
                    <strong>{item.name}</strong>
                    <span>{formatCurrency(item.price)} each</span>
                  </div>
                  <div className="quantity-controls">
                    <button
                      type="button"
                      aria-label={`Decrease ${item.name} quantity`}
                      onClick={() => changeQuantity(item._id, -1)}
                    >
                      -
                    </button>
                    <span>{item.quantity}</span>
                    <button
                      type="button"
                      aria-label={`Increase ${item.name} quantity`}
                      onClick={() => changeQuantity(item._id, 1)}
                    >
                      +
                    </button>
                    <button
                      className="remove-button"
                      type="button"
                      onClick={() => removeFromCart(item._id)}
                    >
                      Remove
                    </button>
                  </div>
                </div>
              ))}
            </div>
          )}

          <div className="cart-total">
            <span>Total</span>
            <strong>{formatCurrency(cartTotal)}</strong>
          </div>
          <button
            className="checkout-button"
            type="button"
            disabled={cart.length === 0 || checkoutBusy}
            onClick={placeOrder}
          >
            {checkoutBusy ? 'Saving order...' : 'Place simulated order'}
          </button>
          {notice && <p className="notice">{notice}</p>}
        </aside>
      </main>
    </div>
  );
}

How does the client use the API?

  • The useEffect() hook calls GET /api/products when the component loads.
  • The loading state keeps the catalog clear while the request is in progress.
  • The placeOrder() function sends product IDs plus quantities to POST /api/orders.
  • A successful response clears the cart and displays the saved order number with the server-calculated total.
  • Save client/src/App.jsx.
  • Switch back to the Microsoft Edge tab from earlier.

The running Vite client refreshes with four products loaded from the API. You will briefly see Loading products... before the product cards appear.

Products not loading?

Confirm that the server terminal still shows the API running at http://localhost:5050. Keep the client development server running at http://localhost:5173.

Check that API_URL matches the server address. A notice inside the cart panel displays the product request problem when the API cannot respond.

Help me find why the NovaCart client cannot load products from the Express API.

Complete the saved checkout

The final check follows one order from the browser to Atlas. The server validates the request before it calculates the price from stored product records.

  • Select Add to cart on the Everyday Backpack card.
  • Select Add to cart on the Desk Lamp card.
  • Increase either product quantity with its + button.

Before you check out, do you expect the confirmation total to come from the browser or from trusted database prices?

  • Select Place simulated order.

You will briefly see Saving order.... The cart then clears and the notice begins with an NC- order number followed by the server-calculated total.

Why calculate the total on the server?

Browser values can be changed before a request reaches the server. NovaCart reloads every selected product from MongoDB so the stored price remains authoritative.

The saved order embeds each product name, price, and quantity. This preserves the checkout snapshot even if a product changes later.

You have completed NovaCart's full MERN request cycle. Products now travel from Atlas to the storefront, while validated orders travel back to the database.

Secret mission

Add Product Search

A fixed catalog becomes harder to browse as more products arrive. Add instant search so shoppers can filter NovaCart by product name or category without disrupting their cart.

Clean Up Your Resources

Clean Up Your Resources

NovaCart's local files and MongoDB Atlas Free cluster have no ongoing cost. Choose whether to keep your resources, stop the local servers for later, or delete everything.

Resources you used:

  • Local novacart folder containing the client and server project files.
  • Two running Windows PowerShell processes for the client and server.
  • MongoDB Atlas Free cluster containing the novacart.products and novacart.orders collections.
  • Atlas database user and current-IP access-list entry.

Keep everything running

No action needed. Choose this if you are still testing the complete NovaCart shopping flow.

  • Keep both PowerShell terminals running while you continue using NovaCart.
  • Keep the novacart folder for future changes.
  • Keep the Atlas Free cluster available at $0 per hour.

Pause - I'll come back to this later

Free clusters cannot be paused manually. Stop both local servers while preserving the project for later.

  • Switch to the PowerShell terminal running the client.
  • Press Ctrl+C to stop the Vite development server.
  • Switch to the PowerShell terminal running the server.
  • Press Ctrl+C to stop the Express API.
  • Leave the novacart folder in its current location.
  • Leave the Atlas Free cluster inactive.

Atlas automatically pauses an inactive Free cluster after 30 days. Your project files remain ready for your return.

Delete - I don't want to use this again

Deleting these resources is permanent. Terminating the Atlas cluster permanently deletes its data.

Remove the local project:

  • Switch to the PowerShell terminal running the client.
  • Press Ctrl+C to stop the Vite development server.
  • Switch to the PowerShell terminal running the server.
  • Press Ctrl+C to stop the Express API.
  • Delete the novacart folder from its current Windows location.
  • Confirm that the novacart folder no longer appears in that location.

Remove the Atlas access resources:

  • Open the database user list for your Atlas project.
  • Remove the database user you created for NovaCart.
  • Open the IP access list for your Atlas project.
  • Remove the current-IP entry you added for NovaCart.

Terminate the Free cluster:

  • Return to Clusters in Atlas.
  • Open the management controls for your Free cluster.
  • Start the cluster termination process.
  • Complete the destructive-action confirmation shown in the Atlas console.
  • Confirm that the terminated cluster no longer appears under Clusters.

Cleanup complete. NovaCart no longer has local files or Atlas resources.

Nice Work!

Nice Work!

Congratulations! Your complete MERN checkout flow now runs from searchable products to a saved simulated order.

What you learned:

  • Built a responsive React storefront with reusable product cards. Managed a live cart through immutable state updates.
  • Connected the storefront to an Express REST API for database-backed product loading. Sent each checkout as a JSON request.
  • Applied server-side validation to every checkout. Recalculated totals from trusted database prices. Persisted embedded order documents in MongoDB Atlas.
  • Completed the Secret Mission by adding instant catalog search across product names or categories.

Ready to quiz yourself?