AI-Powered Product Listing Generator

Build a full-stack app that uses Gemini AI to generate e-commerce listings.

Introduction

30 Second Summary

If you have ever sold anything online, you know the pain of writing product listings. You need a catchy description, persuasive bullet points, and the right keywords to get found in search. Doing this for dozens of products is exhausting.

In this project, you will build a full-stack web application that generates professional e-commerce product listings using AI. You will type in a product name, category, and key features, and Google Gemini will produce a complete listing with a description, marketing bullet points, and SEO tags, all saved to a database you can browse.

What You'll Build

Picture typing "Red Leather Handbag, Accessories, genuine Italian leather, gold hardware" into a form and watching a polished, ready-to-publish product listing appear in seconds.

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

  • A working product listing generator where you enter product details and AI returns a full e-commerce listing with description, bullet points, and SEO keywords displayed in a clean card layout.
  • A REST API you built from scratch with Express that handles creating, reading, and deleting products stored in MongoDB.
  • A React frontend where you can browse your saved product catalog and delete listings you no longer need.
  • Secret Mission: Add search and category filtering so you can find products instantly in a growing catalog.

Are there any prerequisites?

You should have basic HTML, CSS, and JavaScript knowledge. You will also need Node.js and VS Code installed, plus a GitHub account. Step 1 walks you through everything else.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Environment

Your product listing generator needs three foundations: a database to store products, an AI service to generate descriptions, and a local project folder to hold your code.

In this step, you will create a free MongoDB Atlas cluster for persistent storage, grab a Google Gemini API key for AI text generation, and scaffold the project folder structure.

In this step, get ready to:
  • Create a MongoDB Atlas free cluster for product storage.
  • Generate a Google Gemini API key for AI-powered listings.
  • Set up the project folder structure with server and client directories.
Create a MongoDB Atlas account and free cluster

MongoDB Atlas is a cloud-hosted database service. It gives your app a place to save products so they survive server restarts.

  • Open your browser and go to mongodb.com/cloud/atlas/register.
  • Click Sign up with Google (or fill in the email form and click Create your Atlas account).
  • Complete any welcome prompts Atlas shows you.

What is MongoDB Atlas?

Atlas hosts your MongoDB database in the cloud so you do not have to install or run a database on your own machine. The free M0 cluster gives you 512 MB of storage that never expires and requires no credit card.

  • On the cluster creation screen, click the Create button.
  • Select the M0 (Free) option.
  • Choose AWS as your cloud provider.
  • Select the region closest to you from the list.
  • Leave the cluster name as the default (or enter a name you prefer).
  • Click Create to deploy the cluster.

Atlas shows the Security Quickstart wizard. This is where you create your database credentials and allow your computer to connect.

  • Enter a username in the Username field (for example, admin).
  • Enter a password (or copy the one Atlas suggests). Save this password somewhere safe.
  • Click Create Database User.
  • Click Add My Current IP Address to allow your machine to connect.
  • Click Finish and Close.
  • Click Go to Overview.

Your cluster deploys in under 15 seconds. Wait until the status badge shows Active (a green indicator) on the overview page.

  • Click the Connect button on your cluster.
  • Choose the Drivers connection method.
  • Copy the connection string that appears. It looks something like mongodb+srv://admin:<password>@cluster0.xxxxx.mongodb.net/.
  • Paste it into a text file or notes app for safekeeping. Replace <password> with the database user password you just created.

Keep your connection string handy

You will paste this connection string into a .env file in Step 3. For now, just make sure it is saved somewhere you can find it.

Cluster not showing Active?

Wait 15-30 seconds and refresh the page. Free clusters deploy quickly but may take a moment to fully initialize.

If you see a region selection error, try choosing a different region. Not all regions support free clusters.

Help me troubleshoot my MongoDB Atlas cluster deployment.

Get a Google Gemini API key

The Gemini API is what turns basic product details into professional marketing copy. You need an API key to authenticate your requests.

  • Open aistudio.google.com in your browser and sign in with your Google account.
  • Click Get API key in the left panel.
  • Click Create API key.
  • If prompted, select or create a Google Cloud project and confirm.
  • Your new API key appears on screen. Copy it immediately and paste it into a secure note or text file.

Why is this key free?

The Gemini API free tier provides input and output tokens at no charge. Keys generated in AI Studio are restricted to the Gemini API by default, so there is no risk of accidental charges on other Google Cloud services.

Having trouble creating a key?

If AI Studio shows a permissions error, try signing out and signing back in. If the problem persists, you can create a key directly in the Google Cloud Console under APIs & Services > Credentials.

Help me get a Gemini API key working.

Create the project folder structure

Your project has two halves: a server folder for the Express API and a client folder for the React frontend. Creating this structure now keeps your code organized from the start.

  • In VS Code, open the integrated terminal by pressing Cmd+` (macOS) or Ctrl+` (Windows).
  • Move to your Desktop and create the project folder by running these commands:
cd ~/Desktop
mkdir ai-product-listing
cd ai-product-listing
mkdir server client
  • Confirm the folders were created by running:
ls

You should see server and client listed in the output.

  • Initialize the server as a Node.js project by running these commands:
cd server
npm init -y

What does npm init -y do?

This command creates a package.json file that tracks your project's name, version, and dependencies. The -y flag accepts all default values so you do not have to answer prompts.

  • Confirm the file was created by running:
ls

You should see package.json listed in the output.

  • Now open the project folder in VS Code. Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the ai-product-listing folder, then click Open.

VS Code now shows the ai-product-listing folder in the Explorer sidebar with server and client visible inside.

Don't see both folders in VS Code?

Make sure you opened the ai-product-listing folder (the parent), not one of the subfolders. The Explorer panel should show server and client as top-level items.

Help me open the correct folder in VS Code.

✔️ Awesome, I've got everything!

Great. Double check you see package.json inside the server folder in your VS Code Explorer sidebar, then continue.

ⓧ I'd like to double check the full code

Expand the server folder in your Explorer sidebar and click package.json. It should look like this:

{
  "name": "ai-product-listing-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

The name field may vary depending on your npm version. That is fine and will not affect the project.

Your environment is ready. You have a database waiting for data, an AI key ready to generate content, and a project folder structured for full-stack development. Next up, you will build your first Express API endpoint and see it respond in the browser.

Build the Express API with Hardcoded Data

Your development environment is set up with a database ready and an AI key in hand. Now it's time to build the actual application, starting with its backbone: the API.

Before connecting to MongoDB or calling Gemini, you need a working server you can test. Starting with hardcoded responses lets you verify Express is running correctly. When you add the real database later, you will see exactly what changes.

In this step, get ready to:
  • Install Express and its companion packages.
  • Create API routes that return hardcoded product data.
Install backend dependencies and create the Express server

Express needs a few companion packages to work well in a full-stack project. cors lets your React frontend talk to this server. dotenv loads secret values (like API keys) from a file so they stay out of your code.

  • In your terminal (still in the server folder), install the three packages by running:
npm install express cors dotenv

You should see output confirming the packages were added, along with a count of installed packages.

Seeing permission errors or EACCES?

Make sure you are inside the server folder, not the parent ai-product-listing folder. Run ls and confirm you see package.json listed.

help me fix npm permission errors in my project

Now you have the building blocks. Time to wire them together into a running server.

  • Create a new file called index.js inside the server folder.
  • Paste the following code into server/index.js:
const express = require('express');
const cors = require('cors');

// Create the Express app
const app = express();

// Enable CORS so the React frontend can talk to this server
app.use(cors());
// Parse incoming JSON request bodies
app.use(express.json());

// GET route - return all products (hardcoded for now)
app.get('/api/products', (req, res) => {
  res.json([{ id: 1, name: 'Sample Product', category: 'Sample Category', description: 'A sample product description.' }]);
});

// Start the server on port 5000
const PORT = 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

What does this code do?

  • app.use(cors()) allows requests from different origins. Without this, your React app (running on a different port) would be blocked by the browser.
  • app.use(express.json()) tells Express to automatically parse JSON in incoming request bodies, so you can access req.body as a JavaScript object.
  • The GET /api/products route returns a hardcoded array with one sample product. This is a placeholder until you connect the real database.

Before you run this, what do you think will happen when you start the server and visit http://localhost:5000/api/products in your browser?

  • Start the server by running:
node index.js

You should see Server running on port 5000 printed in your terminal.

  • Open your browser and go to http://localhost:5000/api/products.

You should see a JSON array with your sample product object displayed right in the browser. That proves your Express server is receiving requests and sending back data.

Seeing "Cannot GET" or nothing loads?

  • Double-check the URL is exactly http://localhost:5000/api/products with no typos.
  • Make sure your terminal still shows Server running on port 5000 with no error messages below it.
  • If port 5000 is already in use, stop any other process on that port and try again.

help me troubleshoot my Express server not responding on localhost

Add a POST route that accepts product data

Your GET route hands back data, but you also need a way to receive new product details from the frontend. A POST route accepts incoming data in the request body and sends back a response.

  • Stop the running server by pressing Ctrl+C in your terminal.
  • In server/index.js, find the line that says:
// Start the server on port 5000
const PORT = 5000;
  • Add this POST route directly above that line:
// POST route - accept product data and return a placeholder response
app.post('/api/products', (req, res) => {
  const { name, category, features } = req.body;
  res.status(201).json({ name, category, features, description: 'AI-generated description coming soon...' });
});

What does this code do?

  • req.body contains the JSON the client sends. The destructuring pulls out name, category, and features as separate variables.
  • res.status(201) sends a "201 Created" status code, which is the standard response when a new resource is created.
  • The hardcoded description string is a placeholder. In a later step, the Gemini AI will generate the real description.
  • Save server/index.js.
  • Restart your server by running:
node index.js
  • Visit http://localhost:5000/api/products in your browser again to confirm the GET route still returns your sample product.

The sample product JSON appears just like before. Your POST route is ready and waiting for the frontend to send it data.

Server crashes on restart?

  • Check for a missing closing bracket or parenthesis around the new POST route.
  • Make sure the POST route sits above the const PORT = 5000; line, not inside another route.

help me fix a syntax error in my Express server

✔️ Awesome, I've got everything!

Great. Make sure you have saved server/index.js and your server is running with no errors.

ⓧ I'd like to double check the full code

Here is the complete server/index.js at this point:

const express = require('express');
const cors = require('cors');

const app = express();

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

app.get('/api/products', (req, res) => {
  res.json([{ id: 1, name: 'Sample Product', category: 'Sample Category', description: 'A sample product description.' }]);
});

app.post('/api/products', (req, res) => {
  const { name, category, features } = req.body;
  res.status(201).json({ name, category, features, description: 'AI-generated description coming soon...' });
});

const PORT = 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Your API skeleton is alive and responding. Next up, you will swap those hardcoded responses for a real MongoDB database so your products persist between server restarts.

Connect MongoDB and Store Products

Your API works, but it has a big problem: every product lives only in hardcoded JavaScript. Restart the server and nothing persists. A real product catalog needs a database to survive restarts and grow over time.

MongoDB stores data in flexible documents that map directly to JavaScript objects. In this step, you'll connect your Express API to your MongoDB Atlas cluster so products persist in a real database.

In this step, get ready to:
  • Create a Mongoose data model that defines the shape of a product document.
  • Connect your Express server to MongoDB Atlas and replace hardcoded routes with real database operations.
  • Verify that your database connection is live and your API returns real data.
Install Mongoose and create the Product model

Mongoose is an ODM (Object Document Mapper) that lets you define schemas for your MongoDB documents and interact with the database using JavaScript methods.

  • In your terminal, make sure you are inside the server folder, then install Mongoose by running:
npm install mongoose

Seeing permission errors?

Try running the command with sudo on macOS/Linux, or open your terminal as Administrator on Windows.

help me fix npm permission errors when installing mongoose

Now you need a model that tells Mongoose what a product document looks like in the database.

  • Inside the server folder, create a new folder called models.
  • Inside models, create a new file called Product.js.
  • Add the following code to server/models/Product.js:
const mongoose = require('mongoose');

// Define the shape of a product document in MongoDB
const productSchema = new mongoose.Schema({
  name: { type: String, required: true },
  category: { type: String, required: true },
  features: { type: String, required: true },
  description: { type: String },
  bulletPoints: [String],
  seoTags: [String],
  createdAt: { type: Date, default: Date.now }
});

module.exports = mongoose.model('Product', productSchema);

What does this code do?

  • productSchema defines the fields every product document must have (name, category, features) and optional fields that AI will fill later (description, bulletPoints, seoTags).
  • required: true means MongoDB will reject any product missing those fields.
  • createdAt automatically timestamps when a product is saved, which lets you sort by newest first.
  • mongoose.model('Product', productSchema) creates a reusable Product class you can use to create, find, and delete documents.
  • Save server/models/Product.js.
  • Confirm the file exists by running:
ls models

You should see Product.js listed in the output.

Connect to MongoDB Atlas and wire up the routes

Your server needs two things to talk to MongoDB Atlas: the connection string you copied earlier and a way to load it securely from an environment file.

  • In the server folder, create a new file called .env and add the following:
MONGODB_URI=your-mongodb-connection-string-here
PORT=5000

What do I put for MONGODB_URI?

Paste the connection string you copied from MongoDB Atlas in Step 1. It looks like mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/.

Replace username and password with the database user credentials you created earlier.

  • Save .env.

Now you'll rewrite server/index.js to use the database instead of hardcoded data. Replace the entire contents of server/index.js with the following code, starting from the top of the file.

  • Delete all the existing code in server/index.js and paste this first section:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();

// Import the Product model
const Product = require('./models/Product');

const app = express();

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

// Connect to MongoDB Atlas using the URI from .env
mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error('MongoDB connection error:', err));

// GET all products, sorted newest first
app.get('/api/products', async (req, res) => {
  try {
    const products = await Product.find().sort({ createdAt: -1 });
    res.json(products);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch products' });
  }
});

What changed from before?

  • require('dotenv').config() loads your .env file so process.env.MONGODB_URI is available.
  • mongoose.connect() opens a connection to your Atlas cluster when the server starts.
  • The GET route now uses Product.find() to query real documents from MongoDB instead of returning a hardcoded array.
  • async/await handles the database calls, with try/catch to return useful error messages if something goes wrong.
  • Directly below the GET route (still in server/index.js), paste the POST route, DELETE route, and server listener:
// POST a new product to the database
app.post('/api/products', async (req, res) => {
  try {
    const { name, category, features } = req.body;
    if (!name || !category || !features) {
      return res.status(400).json({ error: 'Name, category, and features are required' });
    }
    const product = new Product({ name, category, features });
    await product.save();
    res.status(201).json(product);
  } catch (error) {
    res.status(500).json({ error: 'Failed to create product' });
  }
});

// DELETE a product by its MongoDB ID
app.delete('/api/products/:id', async (req, res) => {
  try {
    await Product.findByIdAndDelete(req.params.id);
    res.json({ message: 'Product deleted' });
  } catch (error) {
    res.status(500).json({ error: 'Failed to delete product' });
  }
});

// Start the server on the port from .env (or default 5000)
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

What do the POST and DELETE routes do?

  • The POST route validates that name, category, and features are present, then creates a new Product document and saves it to MongoDB.
  • The DELETE route takes an :id parameter from the URL and removes that product from the database using findByIdAndDelete().
  • process.env.PORT || 5000 reads the port from your .env file, falling back to 5000 if it is missing.
  • Save server/index.js.

✔️ Awesome, I've got everything!

Great. Double check you have saved both server/.env and server/index.js.

ⓧ I'd like to double check the full code

Your server/models/Product.js should look like this:

const mongoose = require('mongoose');

const productSchema = new mongoose.Schema({
  name: { type: String, required: true },
  category: { type: String, required: true },
  features: { type: String, required: true },
  description: { type: String },
  bulletPoints: [String],
  seoTags: [String],
  createdAt: { type: Date, default: Date.now }
});

module.exports = mongoose.model('Product', productSchema);

Your server/.env should look like this:

MONGODB_URI=your-mongodb-connection-string-here
PORT=5000

Your server/index.js is made of the two code chunks you pasted above. Scroll up and confirm your file contains: the imports and mongoose connection block at the top, then the GET route, then the POST route, the DELETE route, and finally the PORT and app.listen at the bottom.

Test the database connection

Time to see if your server can actually talk to MongoDB Atlas.

  • If your server is still running from the previous step, stop it by pressing Ctrl+C in the terminal.
  • Restart the server by running:
node index.js

Before you look at the output, what do you expect to see in the terminal if your connection string is correct?

You should see two lines printed:

Server running on port 5000
MongoDB connected

Seeing a MongoDB connection error?

  • Double check that your MONGODB_URI in .env has no extra spaces or quotes around the value.
  • Confirm you replaced <password> in the connection string with your actual database user password.
  • Make sure your IP address is whitelisted in MongoDB Atlas (Network Access tab).

help me fix my MongoDB Atlas connection error in Node.js

  • Now open your browser and visit:
http://localhost:5000/api/products

What do you think the response will be, now that you are querying a real (empty) database instead of hardcoded data?

You should see an empty JSON array: []. Your database has no products yet, and that empty array proves your API is reading from MongoDB instead of the old hardcoded list.

Now verify the full round trip by creating a product with curl. Open a new terminal tab (keep the server running) and run:

curl -X POST http://localhost:5000/api/products -H "Content-Type: application/json" -d '{"name": "Test Product", "category": "Testing", "features": "just a test"}'

You should see a JSON response with your product data plus a _id field. That _id is a MongoDB-generated identifier, proving the product was saved to the database.

  • Refresh http://localhost:5000/api/products in your browser.

You should now see an array with your test product in it, complete with the _id and createdAt fields from MongoDB.

GET still returns an empty array after POSTing?

  • Make sure you ran the curl command in a separate terminal tab, not the one running the server.
  • Check the server terminal for any error messages after your POST request.
  • Confirm your curl output includes a _id field. If it returned an error instead, fix the issue and try again.

help me debug why my POST request is not saving to MongoDB

Your API is now connected to a real database. Products persist even if you restart the server. Next up, you'll plug in Google Gemini so that POST request generates a professional AI-written listing before saving.

Integrate Google Gemini AI

Your API saves products to MongoDB, but right now the description, bulletPoints, and seoTags fields are completely empty.

This is the step that makes your app special. You will call the Google Gemini API to transform basic product info into professional marketing-ready content. By the end, typing a product name and features returns a polished e-commerce listing in seconds.

In this step, get ready to:
  • Install the Google GenAI SDK and add your API key to the project.
  • Create an AI generation function that sends a prompt to Gemini and parses the response.
  • Wire the AI function into your POST route so saved products include generated content.
Install the Google GenAI SDK and add your API key

The @google/genai package is Google's official SDK for calling the Gemini API from Node.js.

  • In your terminal, make sure you are inside the server folder, then install the SDK by running:
npm install @google/genai

You should see @google/genai added to your package.json dependencies in the terminal output.

Install failed or timed out?

Make sure you are in the server folder, not the project root. Run ls and confirm you see index.js and package.json listed.

help me debug an npm install failure for @google/genai

  • Open server/.env in VS Code and add your Gemini API key on a new line:
MONGODB_URI=your-mongodb-connection-string-here
GEMINI_API_KEY=your-gemini-api-key-here
PORT=5000
  • Replace your-gemini-api-key-here with the API key you copied from Google AI Studio in Step 1.
  • Save server/.env.

Why use an environment variable for the key?

Storing the key in .env keeps it out of your source code. If you push this project to GitHub later, you can add .env to your .gitignore file so your key is never exposed publicly.

Create the AI generation function

This function takes a product's name, category, and features, sends them to Gemini with a structured prompt, and parses the AI response into fields your database schema expects.

  • Create a new folder called services inside your server folder by running:
mkdir services
  • Create a new file called generateListing.js inside server/services/ and add the following code:
const { GoogleGenAI } = require('@google/genai');

// Initialize the AI client with your API key from .env
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

async function generateListing(name, category, features) {
  // Structured prompt requesting JSON-formatted output
  const prompt = `You are an expert e-commerce copywriter. Generate a professional product listing for the following product:\n\nProduct Name: ${name}\nCategory: ${category}\nKey Features: ${features}\n\nRespond in this exact JSON format (no markdown, no code fences, just raw JSON):\n{\n  "description": "A compelling 2-3 sentence product description",\n  "bulletPoints": ["bullet point 1", "bullet point 2", "bullet point 3", "bullet point 4", "bullet point 5"],\n  "seoTags": ["tag1", "tag2", "tag3", "tag4", "tag5"]\n}`;

  // Send the prompt to Gemini and get the response
  const response = await ai.models.generateContent({
    model: 'gemini-2.5-flash',
    contents: prompt
  });

  // Parse the JSON string from the response into a JavaScript object
  const text = response.text;
  const parsed = JSON.parse(text);
  return parsed;
}

module.exports = generateListing;

What does this code do?

GoogleGenAI is the client class from Google's SDK. You initialize it once with your API key, then call ai.models.generateContent() to send prompts to any Gemini model.

The prompt asks Gemini to respond in raw JSON with specific fields. This means you can call JSON.parse() on the response text and get a clean object with description, bulletPoints, and seoTags ready to save to your database.

  • Save server/services/generateListing.js.
  • Confirm the file exists by running:
ls services

You should see generateListing.js listed in the output.

Wire the AI function into the POST route

The generation function is ready but nothing calls it yet. You need to import it in index.js and call it inside the POST route before saving the product.

  • In server/index.js, find this line near the top:
const Product = require('./models/Product');
  • Add this import directly below it:
const generateListing = require('./services/generateListing');
  • Now find the existing POST route. Replace the entire app.post('/api/products', ...) block with this updated version:
app.post('/api/products', async (req, res) => {
  try {
    const { name, category, features } = req.body;

    if (!name || !category || !features) {
      return res.status(400).json({ error: 'Name, category, and features are required' });
    }

    // Call Gemini AI to generate the listing content
    const listing = await generateListing(name, category, features);

    // Create the product with AI-generated fields included
    const product = new Product({
      name,
      category,
      features,
      description: listing.description,
      bulletPoints: listing.bulletPoints,
      seoTags: listing.seoTags
    });

    await product.save();
    res.status(201).json(product);
  } catch (error) {
    console.error('Error creating product:', error);
    res.status(500).json({ error: 'Failed to generate product listing' });
  }
});

What changed in this route?

Before, the route created a Product with only name, category, and features. Now it calls generateListing() first to get AI-generated content, then includes description, bulletPoints, and seoTags when creating the product.

The error handling now logs the full error with console.error so you can debug AI failures, and returns a descriptive error message to the client.

  • Save server/index.js.
  • Restart your server by pressing Ctrl+C in the terminal, then running:
node index.js

You should see MongoDB connected and Server running on port 5000 in the terminal.

Before you test, what do you think will happen when you send a POST request with product details? Will the response include AI-generated content, or will something go wrong?

  • Open a new terminal window (keep the server running in the first one) and send a test POST request by running:
curl -X POST http://localhost:5000/api/products -H "Content-Type: application/json" -d '{"name": "Red Leather Handbag", "category": "Accessories", "features": "genuine Italian leather, gold hardware, adjustable strap"}'

After a few seconds, you should see a JSON response containing your product with a full AI-generated description, five bulletPoints, and five seoTags. The _id field confirms it was saved to MongoDB.

Getting an error response?

If you see Failed to generate product listing, check the terminal running your server for the full error message.

  • If the error mentions an invalid API key, open server/.env and verify your GEMINI_API_KEY is pasted correctly with no extra spaces.
  • If you see a rate limit error, wait 60 seconds and try again. The Gemini free tier has per-minute request limits.
  • On Windows, single quotes in the curl command may cause issues. Replace the outer single quotes with double quotes and escape the inner ones, or use PowerShell's Invoke-WebRequest instead.

help me debug a failed Gemini API call in my Express route

  • Verify the product was saved to MongoDB by running:
curl http://localhost:5000/api/products

You should see a JSON array containing your Red Leather Handbag product with the AI-generated description, bullet points, and SEO tags all populated.

✔️ Awesome, I've got everything!

Great. Double check you have saved both server/services/generateListing.js and server/index.js.

ⓧ I'd like to double check the full code

Here is the full server/services/generateListing.js:

const { GoogleGenAI } = require('@google/genai');

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

async function generateListing(name, category, features) {
  const prompt = `You are an expert e-commerce copywriter. Generate a professional product listing for the following product:\n\nProduct Name: ${name}\nCategory: ${category}\nKey Features: ${features}\n\nRespond in this exact JSON format (no markdown, no code fences, just raw JSON):\n{\n  "description": "A compelling 2-3 sentence product description",\n  "bulletPoints": ["bullet point 1", "bullet point 2", "bullet point 3", "bullet point 4", "bullet point 5"],\n  "seoTags": ["tag1", "tag2", "tag3", "tag4", "tag5"]\n}`;

  const response = await ai.models.generateContent({
    model: 'gemini-2.5-flash',
    contents: prompt
  });

  const text = response.text;
  const parsed = JSON.parse(text);
  return parsed;
}

module.exports = generateListing;

For server/index.js, scroll up and confirm each section is present in order:

  • Require statements (express, mongoose, cors, dotenv, Product, generateListing).
  • Middleware setup (cors, express.json).
  • MongoDB connection.
  • GET route (find all products, sorted).
  • POST route (validate, call generateListing, create product with AI fields, save).
  • DELETE route (findByIdAndDelete).
  • Server listen on PORT.

And here is server/.env:

MONGODB_URI=your-mongodb-connection-string-here
GEMINI_API_KEY=your-gemini-api-key-here
PORT=5000

Your API now generates professional e-commerce listings powered by AI. Next up, you will build a React frontend so users can interact with your API through a clean web interface instead of terminal commands.

Build the React Frontend

You have a powerful API that generates AI-powered product listings and stores them in MongoDB. But there is no way for a user to interact with it outside of raw HTTP requests.

The React frontend turns your backend into a real application. In this step, you will build a form to input products, a display for AI-generated listings, and a catalog view to browse everything you have created.

In this step, get ready to:
  • Scaffold a React app with Vite and install its dependencies.
  • Build a product form that connects to your API and displays AI-generated listings.
  • Add a product catalog that loads from the database and supports deleting products.
Scaffold the React app and install dependencies

Your project currently has a server folder with the backend. Now you will create a client folder alongside it that holds the React frontend.

  • Open a new terminal window (keep your server terminal running separately).
  • Navigate to your ai-product-listing project root folder (the one containing server) by running:
cd ~/Desktop/ai-product-listing
  • Scaffold a new React app inside a client folder by running:
npm create vite@latest client -- --template react

What does this command do?

This uses Vite's project scaffolder to create a new React app in a folder called client. The --template react flag tells it to use the React starter template so you skip the framework-selection prompt.

The CLI may ask "Install with npm and start now?" Select No so you can install dependencies manually in the next command.

Seeing unexpected prompts?

If the CLI still asks you to choose a framework or variant, the -- separator may not have passed through. Cancel with Ctrl+C and re-run with the --no-interactive flag appended:

npm create vite@latest client -- --template react --no-interactive

Help me bypass the Vite interactive prompts

  • Move into the new client folder and install its dependencies by running:
cd client
npm install

You should see packages installing and a success message once complete.

  • Replace the default src/App.jsx with your product form. Open client/src/App.jsx in VS Code and delete all its contents.
  • Paste the following code into the empty file:
import { useState, useEffect } from 'react';
import './App.css';

function App() {
  // State for the product catalog and form inputs
  const [products, setProducts] = useState([]);
  const [name, setName] = useState('');
  const [category, setCategory] = useState('');
  const [features, setFeatures] = useState('');
  const [loading, setLoading] = useState(false);
  const [generatedProduct, setGeneratedProduct] = useState(null);

  // Base URL for all API calls
  const API_URL = 'http://localhost:5000/api/products';

  return (
    <div className="app">
      <header>
        <h1>AI Product Listing Generator</h1>
        <p>Enter product details and let AI create professional e-commerce listings</p>
      </header>

      <form className="product-form">
        <input type="text" placeholder="Product Name (e.g., Red Leather Handbag)" value={name} onChange={e => setName(e.target.value)} required />
        <input type="text" placeholder="Category (e.g., Accessories)" value={category} onChange={e => setCategory(e.target.value)} required />
        <textarea placeholder="Key Features (e.g., genuine Italian leather, gold hardware, adjustable strap)" value={features} onChange={e => setFeatures(e.target.value)} required />
        <button type="submit" disabled={loading}>{loading ? 'Generating...' : 'Generate Listing'}</button>
      </form>
    </div>
  );
}

export default App;

What does this code do?

This sets up the React component with all the state variables you need: products for the catalog, form field values, a loading flag, and generatedProduct for the latest AI result.

The form renders three inputs (name, category, features) wired to state with onChange handlers. It does not submit yet because you have not added the submit handler.

  • Start the Vite dev server to see your form by running:
npm run dev

Open http://localhost:5173 in your browser. You should see your app header and three form fields with a Generate Listing button.

Don't see the form?

Make sure you saved App.jsx after pasting the code. If you see the default Vite page with a counter, your file was not saved.

Help me fix this

  • Now replace src/App.css with custom styling. Open client/src/App.css in VS Code, delete all its contents, and paste the following:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

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

.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem 1rem;
}

header {
  text-align: center;
  margin-bottom: 2rem;
}
  • Continue adding the heading and form styles below that in App.css:
header h1 {
  font-size: 2rem;
  color: #1a1a2e;
}

header p {
  color: #666;
  margin-top: 0.5rem;
}

.product-form {
  background: white;
  padding: 2rem;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.product-form input,
.product-form textarea {
  padding: 0.75rem 1rem;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 1rem;
}

.product-form textarea {
  min-height: 80px;
  resize: vertical;
}
  • Save App.css and check your browser.

Your form should now appear as a clean white card with rounded inputs, centered on a light grey background.

Form still looks unstyled?

Make sure you deleted the original App.css content before pasting. Leftover styles from the Vite template can conflict. Also check that the import './App.css' line is at the top of your App.jsx.

Help me fix my CSS styles

Connect the form to your API and display results

The form renders but does not do anything when you click Generate. You need a submit handler that sends the form data to your Express API and displays the AI-generated listing that comes back.

  • In App.jsx, add the handleSubmit function below the API_URL line and above the return statement:
  async function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    setGeneratedProduct(null);

    try {
      const res = await fetch(API_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name, category, features })
      });

      const data = await res.json();

      if (res.ok) {
        setGeneratedProduct(data);
        setProducts(prev => [data, ...prev]);
        setName('');
        setCategory('');
        setFeatures('');
      } else {
        alert(data.error || 'Failed to generate listing');
      }
    } catch (error) {
      alert('Failed to connect to server');
    } finally {
      setLoading(false);
    }
  }

What does this code do?

When the form is submitted, handleSubmit sends a POST request to your Express API with the product name, category, and features.

On success, it stores the returned product (with AI-generated content) in generatedProduct for display, adds it to the products array, and clears the form fields. The loading state disables the button while the AI generates.

  • Now wire the handler to the form. Find this line in your App.jsx:
      <form className="product-form">
  • Replace it with this to add the onSubmit handler:
      <form onSubmit={handleSubmit} className="product-form">
  • Save App.jsx and click Generate Listing in your browser.

The button text should change to "Generating..." for a moment, confirming the handler is wired. You may see an alert if the server is not running yet.

  • Next, add the section that displays the AI-generated listing. Paste the following JSX after the closing </form> tag and before the closing </div>:
      {generatedProduct && (
        <div className="generated-result">
          <h2>Generated Listing</h2>
          <h3>{generatedProduct.name}</h3>
          <p className="description">{generatedProduct.description}</p>
          <ul>
            {generatedProduct.bulletPoints.map((point, i) => (
              <li key={i}>{point}</li>
            ))}
          </ul>
          <div className="tags">
            {generatedProduct.seoTags.map((tag, i) => (
              <span key={i} className="tag">{tag}</span>
            ))}
          </div>
        </div>
      )}

What does this code do?

This conditional block only renders when generatedProduct is not null. It displays the product name, description, bullet points as a list, and SEO tags as badge pills.

  • Save App.jsx. Make sure your Express server is still running in the other terminal.
  • In your browser at http://localhost:5173, fill in the form with a test product. Try: Name = Red Leather Handbag, Category = Accessories, Features = genuine Italian leather, gold hardware, adjustable strap.
  • Click Generate Listing.

Before you look at the result, what do you expect to see appear below the form?

After a few seconds, a section should appear below the form showing the product name, a multi-sentence description, five bullet points, and five SEO tags. The form fields clear automatically.

Getting "Failed to connect to server"?

Your Express server must be running at port 5000. Switch to your server terminal and run node index.js if it is not already active.

If you see a CORS error in the browser console, make sure app.use(cors()) is in your server/index.js.

Help me debug my server connection

  • Add the button and result card styles to App.css. Paste this below the existing styles:
.product-form button {
  padding: 0.75rem;
  background: #4361ee;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  cursor: pointer;
  transition: background 0.2s;
}

.product-form button:hover {
  background: #3a56d4;
}

.product-form button:disabled {
  background: #a0a0a0;
  cursor: not-allowed;
}

.generated-result {
  background: #e8f5e9;
  padding: 1.5rem;
  border-radius: 12px;
  margin-top: 1.5rem;
  border: 1px solid #c8e6c9;
}
  • Continue adding the result text and tag styles below that in App.css:
.generated-result h2 {
  color: #2e7d32;
  margin-bottom: 0.5rem;
}

.generated-result h3 {
  margin-bottom: 0.5rem;
}

.description {
  margin-bottom: 1rem;
  line-height: 1.6;
}

ul {
  padding-left: 1.5rem;
  margin-bottom: 1rem;
}

li {
  margin-bottom: 0.25rem;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.tag {
  background: #e3f2fd;
  color: #1565c0;
  padding: 0.25rem 0.75rem;
  border-radius: 16px;
  font-size: 0.85rem;
}
  • Save App.css and generate another listing to confirm the result card has a green background with formatted bullet points and blue tag pills.
Add the product catalog view

Every product you generate is stored in MongoDB, but you can only see the most recent one. Now you will add a catalog section that fetches all products on page load and lets you delete ones you no longer need.

  • In App.jsx, add the useEffect, fetchProducts, and handleDelete functions. Place them below the API_URL line and above your handleSubmit function:
  useEffect(() => {
    fetchProducts();
  }, []);

  async function fetchProducts() {
    try {
      const res = await fetch(API_URL);
      const data = await res.json();
      setProducts(data);
    } catch (error) {
      console.error('Failed to fetch products:', error);
    }
  }

  async function handleDelete(id) {
    try {
      await fetch(`${API_URL}/${id}`, { method: 'DELETE' });
      setProducts(prev => prev.filter(p => p._id !== id));
    } catch (error) {
      alert('Failed to delete product');
    }
  }

What does this code do?

The useEffect hook calls fetchProducts once when the component first loads. This fetches all existing products from your GET endpoint and populates the catalog.

The handleDelete function sends a DELETE request for a specific product ID, then removes it from the local state so the UI updates instantly without a page refresh.

  • Now add the catalog section JSX. Paste this after the generatedProduct block's closing )} and before the final </div>:
      <section className="catalog">
        <h2>Product Catalog ({products.length})</h2>
        {products.length === 0 && <p>No products yet. Generate your first listing above.</p>}
        {products.map(product => (
          <div key={product._id} className="product-card">
            <div className="card-header">
              <h3>{product.name}</h3>
              <span className="category-badge">{product.category}</span>
            </div>
            <p>{product.description}</p>
            <ul>
              {product.bulletPoints.map((point, i) => (
                <li key={i}>{point}</li>
              ))}
            </ul>
            <div className="tags">
              {product.seoTags.map((tag, i) => (
                <span key={i} className="tag">{tag}</span>
              ))}
            </div>
            <button className="delete-btn" onClick={() => handleDelete(product._id)}>
              Delete
            </button>
          </div>
        ))}
      </section>

What does this code do?

This maps over every product in state and renders a card showing the name, category badge, description, bullet points, SEO tags, and a Delete button. The count in the heading updates as products are added or removed.

  • Save App.jsx and check your browser. You should see a "Product Catalog" section below the form. If you generated a product earlier, it appears here as a card.
  • Add the catalog and card styles to the bottom of App.css:
.catalog {
  margin-top: 2rem;
}

.catalog h2 {
  margin-bottom: 1rem;
}

.product-card {
  background: white;
  padding: 1.5rem;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  margin-bottom: 1rem;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.75rem;
}

.category-badge {
  background: #f3e5f5;
  color: #7b1fa2;
  padding: 0.25rem 0.75rem;
  border-radius: 16px;
  font-size: 0.8rem;
}
  • Add the delete button styles below that:
.delete-btn {
  margin-top: 1rem;
  padding: 0.5rem 1rem;
  background: #ffebee;
  color: #c62828;
  border: 1px solid #ef9a9a;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.85rem;
}

.delete-btn:hover {
  background: #ffcdd2;
}
  • Save App.css.

Before you refresh, what do you expect the catalog to look like now?

Refresh your browser. Each product card should have a white background, a purple category badge in the top right, and a red Delete button at the bottom. Click Delete on a product and it disappears from the list instantly.

Catalog shows 0 products?

The catalog fetches from your Express server on page load. If the server is not running, the fetch silently fails and you get an empty array. Check your server terminal for "MongoDB connected" output.

Help me debug my empty catalog

✔️ 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

Your App.jsx should contain these sections in order (scroll up to confirm each is present):

  • Imports and state declarations (useState, useEffect, six state variables, API_URL).
  • The useEffect calling fetchProducts on mount.
  • The fetchProducts async function.
  • The handleDelete async function.
  • The handleSubmit async function.
  • The return block with: header, form (with onSubmit={handleSubmit}), generated-result conditional, and catalog section.

Your App.css should contain styles in this order:

  • Global reset (*), body, .app, header styles.
  • Form styles (.product-form, inputs, textarea, button, hover, disabled).
  • Generated result styles (.generated-result, description, ul, li, tags, tag).
  • Catalog styles (.catalog, .product-card, .card-header, .category-badge, .delete-btn).

Your full-stack app is alive. You can type a product, watch AI generate a professional listing, browse your catalog, and delete entries you no longer need.

Secret mission

Add Search and Category Filtering

Your catalog works, but scrolling through dozens of products to find one is painful. Real e-commerce tools have search and filters. Can you add client-side filtering that lets users search by name or description AND filter by category at the same time?

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 uses MongoDB Atlas's free tier and the Gemini API free tier, so there are no ongoing costs.

Resources you used:

  • MongoDB Atlas free cluster (M0) storing your product catalog.
  • Google Gemini API key used for generating product listings.
  • Local project files in the ai-product-listing folder on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or want to demo the app later.

  • Your Atlas M0 cluster is free forever (512 MB storage). It will auto-pause after 30 days of zero connections and can be resumed at any time.
  • Your Gemini API key remains on the free tier with rate limits. No charges will be incurred at this project's usage level.
  • Your local ai-product-listing folder stays on your Desktop for future edits.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and cloud resources stay intact.

  • Stop the Express server by pressing Ctrl+C in the terminal where it is running.
  • Stop the Vite dev server by pressing Ctrl+C in the terminal where it is running.
  • Your Atlas cluster will auto-pause after 30 days of no connections. No action needed.
  • Your Gemini API key stays valid but costs nothing while unused.

Delete - I don't want to use this again

Remove all project resources and start fresh. This is permanent.

  • Delete the MongoDB Atlas cluster: go to the Atlas dashboard, click Clusters in the left sidebar, click the ellipsis (...) next to your cluster, select Terminate, enter the cluster name, and click Terminate to confirm.
  • Delete the Gemini API key: open the Google Cloud API Credentials page, find your API key, click the Actions icon, select Delete API key, then confirm by clicking Delete in the modal.
  • Delete the local project files by running these commands (macOS/Linux):
cd ~/Desktop
rm -rf ai-product-listing
  • On Windows, run:
cd ~\Desktop
rmdir /s /q ai-product-listing

That's a Wrap!

That's a Wrap!

Nice work! You just built a full-stack web application that turns basic product details into professional e-commerce listings powered by AI.

You've learned how to:

  • Built a React frontend with forms, state management, and dynamic rendering that connects to a REST API you created with Express.
  • Connected your app to MongoDB Atlas and modeled product data with Mongoose schemas so your catalog persists across server restarts.
  • Integrated the Google Gemini AI API to generate compelling product descriptions, marketing bullet points, and SEO tags from basic input.
  • Secret Mission: Added interactive search and category filtering to the product catalog using client-side filtering with multiple simultaneous criteria.

Ready to quiz yourself?