AI-Powered Customer Feedback Analyzer
Build a full-stack CRUD app with AI sentiment analysis via Gemini.
Introduction
30 Second Summary
Every business collects customer feedback. Manually reading through hundreds of messages to understand how customers feel takes forever.
In this project, you will build a full-stack feedback analyzer powered by Google's Gemini AI. It automatically detects the sentiment behind each message, extracts key themes, and drafts a professional response.
What You'll Build
A customer feedback management dashboard with a React frontend, a Node.js/Express backend, and AI-powered analysis via the Gemini API.
By the end of this project, you'll have:
- A clean feedback submission form and feedback list that display AI-analyzed customer entries.
- A full REST API with CRUD endpoints for creating, reading, updating, and deleting feedback.
- AI-powered sentiment analysis, theme extraction, and response generation running on each new submission via the Gemini API.
- Secret Mission: A dynamic sentiment dashboard showing breakdown statistics across all feedback entries.
Are there any prerequisites?
This project assumes basic familiarity with JavaScript, Node.js, and React. You should also have Node.js and VS Code installed on your computer.
Set Up Your Project and Get a Gemini API Key
Every full-stack application needs a clean foundation before you write any features. You need a backend server to handle requests, a frontend framework to build the user interface, and access to the AI service that powers the intelligent features.
In this step, you'll create your project folder, set up a Node.js backend with Express, scaffold a React frontend with Vite, and grab a free API key from Google AI Studio.
In this step, get ready to:
- Create the project structure and initialize the backend.
- Scaffold the React frontend with Vite.
- Get a free Gemini API key from Google AI Studio.
Create the project structure and initialize the backend
Your project will have two separate folders: a server/ folder for the Express backend and a client/ folder for the React frontend.
- In VS Code, open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and server subdirectory by running these commands:
mkdir feedback-analyzer
cd feedback-analyzer
mkdir server
cd server
- Initialize a new Node.js project by running:
npm init -y
What does npm init -y do?
This command creates a package.json file that tracks your project's metadata and dependencies. The -y flag accepts all the default values so you don't have to answer prompts.
The generated package.json has some defaults you don't need. You'll replace its contents with a clean configuration.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the feedback-analyzer folder on your Desktop, then click Open.
- If VS Code asks whether you trust the authors of the files, click Yes, I trust the authors.
- In the Explorer sidebar on the left, expand the server folder and click package.json to open it.
- Select all the text in the file and replace it with the following code:
{
"name": "feedback-analyzer-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node --env-file=.env index.js"
}
}
What does this configuration do?
There are two key changes from the defaults:
- "type": "module" tells Node.js to use modern ES module syntax (import/export) instead of the older require() style. The Google AI SDK requires this.
- The start script uses the --env-file=.env flag to automatically load environment variables from a .env file when you run the server. This avoids needing an extra package like dotenv.
- Save package.json by pressing Ctrl+S (Windows) or Cmd+S (macOS).
Now install the three backend packages your server needs.
- Open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Navigate into the server folder and install the dependencies by running these commands:
cd server
npm install express@4.19.2 cors@2.8.6 @google/genai@2.10.0
What are these packages?
- express is a web server framework that handles HTTP requests and routing.
- cors allows your React frontend (running on port 5173) to talk to your Express backend (running on port 3001).
- @google/genai is Google's official SDK for calling the Gemini AI models.
- Confirm the installation succeeded by running:
node -e "require('express')"
If the command completes with no output and no errors, Express is installed correctly.
Seeing an error?
Make sure you ran npm install from inside the server folder, not the project root. You can check your current location by running pwd (macOS) or cd (Windows). It should end with feedback-analyzer/server.
Help me fix my npm install error in the feedback-analyzer server folder.
Finally, create the environment file where your API key will live.
- In the Explorer sidebar, right-click the server folder and select New File.
- Name the file .env and press Enter.
- Add this placeholder line to the file:
GEMINI_API_KEY=your-api-key-here
- Save .env.
You'll replace your-api-key-here with a real key in the last substep.
✔️ Awesome, I've got everything!
Great. Double check you've saved both server/package.json and server/.env.
ⓧ I'd like to double check the full code
Here is what server/package.json should look like after installing the dependencies. Your file may include a ^ before each version number, which is normal.
{
"name": "feedback-analyzer-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node --env-file=.env index.js"
},
"dependencies": {
"express": "4.19.2",
"cors": "2.8.6",
"@google/genai": "2.10.0"
}
}
Here is what server/.env should look like:
GEMINI_API_KEY=your-api-key-here
Scaffold the React frontend
Your frontend will be a React app built with Vite, a fast development tool that gives you instant browser refreshes as you code.
- In the terminal, navigate back to the project root by running:
cd ..
- Scaffold the React app by running:
npm create vite@latest client -- --template react
What does this command do?
This creates a new React project inside a client/ folder with all the starter files you need. The --template react flag tells Vite to set up a React project specifically.
If the CLI asks any follow-up questions, accept the defaults by pressing Enter.
- Navigate into the client folder and install its dependencies by running:
cd client
npm install
Vite's template includes demo content you don't need. You'll clear out the two main files and start fresh.
- In the Explorer sidebar, expand client > src and click App.jsx to open it.
- Select all the text and replace it with the following code:
// Cleared of boilerplate
export default function App() {
return <></>
}
Why clear the boilerplate?
Vite's template comes with demo logos, counter buttons, and pre-written CSS. Clearing these files gives you a blank canvas so every line of code in the final app is something you wrote and understand.
- Save App.jsx.
- Open App.css in the same src folder.
- Select all the text and replace it with:
/* Cleared of boilerplate */
- Save App.css.
- Start the Vite development server by running this command in the terminal:
npm run dev
- Open your browser and go to http://localhost:5173.
You should see a blank white page. That means your React app is running and ready for your custom code.
Don't see a blank page?
If you see Vite's default landing page with logos and a counter, make sure you saved both App.jsx and App.css after clearing them. Vite auto-refreshes when you save.
If the terminal shows an error about the port being in use, stop any other dev servers running on port 5173 and try again.
Help me get my Vite dev server running at localhost:5173.
✔️ Awesome, I've got everything!
Great. Make sure you've saved both App.jsx and App.css.
ⓧ I'd like to double check the full code
Here is what client/src/App.jsx should look like:
// Cleared of boilerplate
export default function App() {
return <></>
}
Here is what client/src/App.css should look like:
/* Cleared of boilerplate */
Get your free Gemini API key
The Gemini API is Google's AI service that will power the sentiment analysis in your app. You need an API key to authenticate your requests. The free tier is generous and requires no credit card.
- Open your browser and go to https://aistudio.google.com.
- Sign in with your Google account.
- Accept the Terms of Service if prompted.
What happens for new users?
If this is your first time using Google AI Studio, it automatically creates a default Google Cloud project and an API key for you after you accept the Terms of Service. You may already see a key on the API Keys page.
- Click Get API key in the left panel.
- Click Create API key.
- If prompted to select a project, choose the default project and click Create API key in existing project.
- Copy the API key that appears. You will only see the full key once, so make sure to copy it now.
Keep your API key private
Never commit your .env file to version control or share your key publicly. The .env file stays on your local machine only.
- Switch back to VS Code and open server/.env.
- Replace your-api-key-here with the key you just copied. The line should look like:
GEMINI_API_KEY=your-api-key-here
Replace your-api-key-here in the code above with the actual key you copied from Google AI Studio.
- Save .env.
Having trouble creating an API key?
Make sure you are signed in with a personal Google account. Workplace or school accounts managed by an organization may have restrictions that prevent API key creation.
If you see a permissions error, try creating the key from the Google Cloud Console instead at console.cloud.google.com/apis/credentials.
Help me get my Gemini API key set up.
Your development environment is ready. Next up, you'll build the Express REST API with full CRUD endpoints for managing customer feedback.
Build the Express REST API
Your project environment is ready with both folders in place. Now it's time to build the backend server that will receive, store, and manage customer feedback.
Every piece of feedback needs to be created, read, updated, and deleted. These four operations form the backbone of any data-driven application. You'll use Express to build a REST API that handles all four.
In this step, get ready to:
- Set up an Express server with middleware and in-memory storage.
- Implement all four CRUD routes for feedback entries.
Create the Express server with in-memory storage
Before you can add any routes, you need a running server that can accept requests. You also need somewhere to store feedback data.
Instead of setting up a database, you'll use an in-memory JavaScript array. This keeps things simple and lets you focus on the API logic.
- In VS Code, right-click the server folder in the Explorer sidebar.
- Select New File and name it index.js.
- Paste the following code into server/index.js:
import express from 'express';
import cors from 'cors';
const app = express();
const PORT = 3001;
// Allow the React frontend to make requests to this server
app.use(cors());
// Parse incoming JSON request bodies
app.use(express.json());
// In-memory storage for feedback entries
let feedbackEntries = [];
let nextId = 1;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What does this code do?
- express creates a web server. cors allows the React frontend (running on port 5173) to talk to this server (running on port 3001).
- express.json() is middleware that automatically parses JSON data from incoming requests so you can access it in your route handlers.
- feedbackEntries is a plain JavaScript array that acts as your data store. nextId is a counter that gives each new entry a unique ID.
Why an in-memory array instead of a database?
A database would add signup steps, connection strings, and extra dependencies. The in-memory array lets you focus on learning CRUD patterns and AI integration without that overhead. The tradeoff is that your data resets every time the server restarts.
- Save server/index.js.
- Open a terminal in VS Code by pressing Ctrl+` (macOS and Windows).
- Navigate to the server folder and start the server by running these commands:
cd server
npm start
You should see Server running on http://localhost:3001 printed in your terminal.
Server won't start?
- Make sure you're in the server folder, not the project root or client. Run pwd to check.
- If you see a syntax error about import, confirm that your server/package.json contains "type": "module".
- If you see a missing module error, run npm install in the server folder to reinstall dependencies.
Still stuck? Help me debug why my Express server won't start.
Implement all CRUD routes
CRUD stands for Create, Read, Update, and Delete. Each operation maps to an HTTP method: POST, GET, PUT, and DELETE.
You'll add four route handlers to your server. Each one handles a different operation on the feedbackEntries array.
- Stop the running server by pressing Ctrl+C in the terminal.
- In server/index.js, find this line:
app.listen(PORT, () => {
- Add the GET and POST routes directly above that app.listen line by pasting this code:
// GET all feedback
app.get('/api/feedback', (req, res) => {
res.json(feedbackEntries);
});
// POST new feedback
app.post('/api/feedback', async (req, res) => {
const { customerName, feedbackText } = req.body;
if (!customerName || !feedbackText) {
return res.status(400).json({ error: 'customerName and feedbackText are required' });
}
// Placeholder analysis values (AI integration comes in the next step)
const entry = {
id: nextId++,
customerName,
feedbackText,
sentiment: 'unknown',
themes: [],
suggestedResponse: '',
createdAt: new Date().toISOString(),
};
feedbackEntries.push(entry);
res.status(201).json(entry);
});
What does this code do?
- The GET route at /api/feedback returns the entire feedbackEntries array as JSON. This is the "Read" in CRUD.
- The POST route is the "Create" operation. It pulls customerName and feedbackText from the request body, validates that both exist, and builds a new entry object.
- The sentiment, themes, and suggestedResponse fields are placeholders for now. You'll replace them with real AI analysis in the next step.
- Save server/index.js.
You won't see a change yet. The next two chunks add the remaining routes before you restart the server.
- Add the PUT route directly below the POST route you just pasted (and above app.listen):
// PUT update feedback
app.put('/api/feedback/:id', (req, res) => {
const id = parseInt(req.params.id);
const entry = feedbackEntries.find((e) => e.id === id);
if (!entry) {
return res.status(404).json({ error: 'Feedback not found' });
}
const { customerName, feedbackText } = req.body;
if (customerName) entry.customerName = customerName;
if (feedbackText) entry.feedbackText = feedbackText;
res.json(entry);
});
What does this code do?
- The :id in the URL is a route parameter. Express captures whatever value appears there and makes it available through req.params.id.
- The handler finds the matching entry by ID, then updates only the fields that were sent in the request body. If no entry matches, it returns a 404 error.
- Save server/index.js.
One more route to go. The DELETE route will complete your CRUD set.
- Add the DELETE route directly below the PUT route you just pasted (and above app.listen):
// DELETE feedback
app.delete('/api/feedback/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = feedbackEntries.findIndex((e) => e.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Feedback not found' });
}
feedbackEntries.splice(index, 1);
res.status(204).send();
});
What does this code do?
- findIndex() locates the entry's position in the array. If it's not found, the handler returns a 404 error.
- splice() removes the entry from the array. The response uses status 204 (No Content), which is the standard HTTP response for a successful deletion with no body to return.
- Save server/index.js.
- Restart the server by running this command in the terminal:
npm start
- Open a second terminal tab by pressing Ctrl+Shift+` (macOS and Windows).
- Test the GET route by running this command:
curl http://localhost:3001/api/feedback
You should see an empty array [] printed in the terminal. This confirms that the server is running and the GET route is responding correctly.
Not seeing an empty array?
- If you see a connection refused error, make sure the server is still running in the other terminal tab. Run npm start from the server folder.
- If you see a syntax error when starting the server, double-check that each route block has matching opening and closing braces and parentheses.
- Make sure you pasted the routes above the app.listen line, not below it. Routes registered after app.listen still work, but placing them above keeps the code organized.
Help me debug why my curl request to localhost:3001 isn't returning an empty array.
✔️ Awesome, I've got everything!
Great. Double check you've saved server/index.js and that your server is still running.
ⓧ I'd like to double check the full code
Here's the complete server/index.js file. Compare it with yours line by line.
import express from 'express';
import cors from 'cors';
const app = express();
const PORT = 3001;
app.use(cors());
app.use(express.json());
let feedbackEntries = [];
let nextId = 1;
app.get('/api/feedback', (req, res) => {
res.json(feedbackEntries);
});
app.post('/api/feedback', async (req, res) => {
const { customerName, feedbackText } = req.body;
if (!customerName || !feedbackText) {
return res.status(400).json({ error: 'customerName and feedbackText are required' });
}
const entry = {
id: nextId++,
customerName,
feedbackText,
sentiment: 'unknown',
themes: [],
suggestedResponse: '',
createdAt: new Date().toISOString(),
};
feedbackEntries.push(entry);
res.status(201).json(entry);
});
app.put('/api/feedback/:id', (req, res) => {
const id = parseInt(req.params.id);
const entry = feedbackEntries.find((e) => e.id === id);
if (!entry) {
return res.status(404).json({ error: 'Feedback not found' });
}
const { customerName, feedbackText } = req.body;
if (customerName) entry.customerName = customerName;
if (feedbackText) entry.feedbackText = feedbackText;
res.json(entry);
});
app.delete('/api/feedback/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = feedbackEntries.findIndex((e) => e.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Feedback not found' });
}
feedbackEntries.splice(index, 1);
res.status(204).send();
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Your REST API is live with all four CRUD operations. Next up, you'll connect the Gemini AI to analyze every new piece of feedback the moment it's submitted.
Integrate Google Gemini for AI Analysis
Your REST API can create, read, update, and delete feedback entries. Right now though, every entry gets a hardcoded sentiment of unknown and empty themes. The data goes in, but nothing intelligent comes back out.
In this step, you will connect the Gemini API to your server so that every new piece of feedback is automatically analyzed for sentiment, key themes, and a suggested response. By the end of this step, a single POST request will return real AI-generated insights.
In this step, get ready to:
- Create a Gemini client module that sends feedback text to the AI and parses the response.
- Wire the AI analysis into your POST route so every new entry gets real sentiment, themes, and a suggested response.
Set up the Gemini client
The @google/genai package you installed in Step 1 provides a GoogleGenAI class that handles authentication and API calls to Google's Gemini models. You will wrap this in a dedicated module so the rest of your server can call one clean function.
- Create a new file called gemini.js inside the server/ folder and add the following code:
import { GoogleGenAI } from '@google/genai';
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
export async function analyzeFeedback(feedbackText) {
const prompt = `Analyze the following customer feedback and return a JSON object with exactly these fields:
- "sentiment": one of "positive", "neutral", or "negative"
- "themes": an array of 1-3 key themes (short phrases)
- "suggestedResponse": a professional, empathetic response to the customer (2-3 sentences)
Return ONLY the JSON object, no markdown formatting, no code blocks.
Customer feedback: "${feedbackText}"`;
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash',
contents: prompt,
});
const text = response.text;
const parsed = JSON.parse(text);
return {
sentiment: parsed.sentiment || 'unknown',
themes: parsed.themes || [],
suggestedResponse: parsed.suggestedResponse || '',
};
}
What does this code do?
new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }) creates an authenticated client using the API key from your .env file. The node --env-file=.env flag in your start script loads these values automatically.
The analyzeFeedback function builds a prompt that tells Gemini to return a JSON object with three fields: sentiment, themes, and suggestedResponse. The prompt explicitly asks for raw JSON with no markdown formatting so JSON.parse() can parse the response cleanly.
The return statement uses fallback values (e.g. parsed.sentiment || 'unknown') so that even if Gemini omits a field, the function still returns a complete object.
- Save server/gemini.js.
- Confirm the file was created by running this in your terminal (from the server/ folder):
ls
You should see gemini.js alongside index.js, package.json, and .env in the output.
Don't see gemini.js in the list?
- Make sure you created the file inside the server/ folder, not in the project root or the client/ folder.
- Check that your terminal is in the server/ directory when you run ls.
- help me find where my gemini.js file ended up
✔️ Awesome, I've got everything!
Great. Make sure you have saved server/gemini.js before moving on.
ⓧ I'd like to double check the full code
Here is the complete server/gemini.js file for comparison:
import { GoogleGenAI } from '@google/genai';
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
export async function analyzeFeedback(feedbackText) {
const prompt = `Analyze the following customer feedback and return a JSON object with exactly these fields:
- "sentiment": one of "positive", "neutral", or "negative"
- "themes": an array of 1-3 key themes (short phrases)
- "suggestedResponse": a professional, empathetic response to the customer (2-3 sentences)
Return ONLY the JSON object, no markdown formatting, no code blocks.
Customer feedback: "${feedbackText}"`;
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash',
contents: prompt,
});
const text = response.text;
const parsed = JSON.parse(text);
return {
sentiment: parsed.sentiment || 'unknown',
themes: parsed.themes || [],
suggestedResponse: parsed.suggestedResponse || '',
};
}
Wire AI analysis into the POST route
The analyzeFeedback function is ready, but your POST route does not call it yet. You need to import the function into server/index.js and update the handler to run AI analysis on every new submission.
- In server/index.js, find the import lines at the top of the file:
import express from 'express';
import cors from 'cors';
- Add a third import line for analyzeFeedback so the imports look like this:
import express from 'express';
import cors from 'cors';
import { analyzeFeedback } from './gemini.js';
- Now scroll down and find the POST handler. It starts with this line:
app.post('/api/feedback', async (req, res) => {
- Replace the entire POST handler (from app.post down to its closing });) with this updated version:
app.post('/api/feedback', async (req, res) => {
const { customerName, feedbackText } = req.body;
if (!customerName || !feedbackText) {
return res.status(400).json({ error: 'customerName and feedbackText are required' });
}
let analysis = { sentiment: 'unknown', themes: [], suggestedResponse: '' };
try {
analysis = await analyzeFeedback(feedbackText);
} catch (error) {
console.error('AI analysis failed:', error.message);
}
const entry = {
id: nextId++,
customerName,
feedbackText,
sentiment: analysis.sentiment,
themes: analysis.themes,
suggestedResponse: analysis.suggestedResponse,
createdAt: new Date().toISOString(),
};
feedbackEntries.push(entry);
res.status(201).json(entry);
});
What changed in the POST handler?
The handler now starts with a fallback analysis object containing safe defaults: sentiment: 'unknown', an empty themes array, and an empty suggestedResponse string.
Inside the try/catch block, it calls analyzeFeedback(feedbackText) and overwrites the defaults with real AI results. If the API call fails for any reason (bad key, rate limit, network issue), the catch block logs the error and the entry is still saved with the fallback values. The feedback never gets lost.
Why use error handling here?
AI APIs are external services. They can fail due to network issues, rate limits, or invalid keys. Wrapping the call in a try/catch ensures your server keeps working even when the AI is unavailable. The feedback is always saved, and the AI analysis is a bonus on top.
- Save server/index.js.
- Switch to the terminal where your server is running and press Ctrl+C to stop it.
- Restart the server by running:
npm start
- Test the AI integration by submitting a POST request with curl. Open a second terminal window, navigate to your server/ folder, and run:
curl -X POST http://localhost:3001/api/feedback \
-H "Content-Type: application/json" \
-d '{"customerName": "Alice", "feedbackText": "Great product, fast shipping!"}'
You should see a JSON response that includes AI-generated values for sentiment (like positive), themes (like ["product quality", "shipping speed"]), and a suggestedResponse with a professional reply.
Seeing an error instead of AI results?
- If you see sentiment: "unknown" in the response, the AI call failed silently. Check your terminal running the server for an error message. The most common cause is a missing or invalid API key in server/.env.
- If you see a crash mentioning Cannot find module './gemini.js', make sure gemini.js is in the same server/ folder as index.js.
- Make sure you stopped and restarted the server with npm start after saving your changes. The server does not reload automatically.
- help me debug why my AI analysis is not working
✔️ Awesome, I've got everything!
Great. Double check you have saved both server/gemini.js and server/index.js.
ⓧ I'd like to double check the full code
Here is the complete server/index.js file for comparison:
import express from 'express';
import cors from 'cors';
import { analyzeFeedback } from './gemini.js';
const app = express();
const PORT = 3001;
app.use(cors());
app.use(express.json());
let feedbackEntries = [];
let nextId = 1;
// GET all feedback
app.get('/api/feedback', (req, res) => {
res.json(feedbackEntries);
});
// POST new feedback
app.post('/api/feedback', async (req, res) => {
const { customerName, feedbackText } = req.body;
if (!customerName || !feedbackText) {
return res.status(400).json({ error: 'customerName and feedbackText are required' });
}
let analysis = { sentiment: 'unknown', themes: [], suggestedResponse: '' };
try {
analysis = await analyzeFeedback(feedbackText);
} catch (error) {
console.error('AI analysis failed:', error.message);
}
const entry = {
id: nextId++,
customerName,
feedbackText,
sentiment: analysis.sentiment,
themes: analysis.themes,
suggestedResponse: analysis.suggestedResponse,
createdAt: new Date().toISOString(),
};
feedbackEntries.push(entry);
res.status(201).json(entry);
});
// PUT update feedback
app.put('/api/feedback/:id', (req, res) => {
const id = parseInt(req.params.id);
const entry = feedbackEntries.find((e) => e.id === id);
if (!entry) {
return res.status(404).json({ error: 'Feedback not found' });
}
const { customerName, feedbackText } = req.body;
if (customerName) entry.customerName = customerName;
if (feedbackText) entry.feedbackText = feedbackText;
res.json(entry);
});
// DELETE feedback
app.delete('/api/feedback/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = feedbackEntries.findIndex((e) => e.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Feedback not found' });
}
feedbackEntries.splice(index, 1);
res.status(204).send();
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Your backend is now powered by AI. Every feedback submission gets analyzed for sentiment, themes, and a suggested response before it is saved. Next up, you will build the React frontend to bring all of this to life in the browser.
Build the React Frontend
Your backend is ready. It can store feedback entries, run CRUD operations, and call the Gemini API for AI analysis. But right now, the only way to interact with it is through curl commands in the terminal.
In this step, you will build a React frontend that connects to your API. You will create a form for submitting feedback, a card-based list for viewing entries with AI insights, and professional styling to pull it all together.
In this step, get ready to:
- Create a feedback submission form that posts data to your API.
- Build a feedback list with AI-powered sentiment badges, theme tags, and suggested responses.
- Style the app with a clean, professional layout.
Create the feedback submission form
The form needs two inputs: the customer's name and their feedback text. When submitted, it sends a POST request to your API and shows a loading state while the AI processes the feedback.
- In VS Code, open client/src/App.jsx.
- Select all the existing content and replace it with the following code:
import { useState, useEffect } from 'react';
import './App.css';
// API endpoint for the Express backend
const API_URL = 'http://localhost:3001/api/feedback';
function FeedbackForm({ onSubmit }) {
const [customerName, setCustomerName] = useState('');
const [feedbackText, setFeedbackText] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
// Don't submit if either field is empty
if (!customerName.trim() || !feedbackText.trim()) return;
setLoading(true);
try {
// Send feedback to the backend
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customerName, feedbackText }),
});
const data = await res.json();
// Pass the new entry up to the parent component
onSubmit(data);
// Clear the form fields
setCustomerName('');
setFeedbackText('');
} catch (error) {
console.error('Failed to submit feedback:', error);
} finally {
setLoading(false);
}
};
return (
<form className="feedback-form" onSubmit={handleSubmit}>
<h2>Submit Feedback</h2>
<input
type="text"
placeholder="Customer Name"
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
required
/>
<textarea
placeholder="Enter customer feedback..."
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
rows={4}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Analyzing with AI...' : 'Submit Feedback'}
</button>
</form>
);
}
What does this code do?
API_URL is a constant that points to your Express backend at http://localhost:3001/api/feedback. Every component in this file uses it to make API calls.
FeedbackForm tracks three pieces of state: the customer name, the feedback text, and whether the form is currently loading. When the learner clicks submit, it POSTs the data to your API and calls onSubmit with the response so the parent component can update its list.
The button text switches to "Analyzing with AI..." while loading, giving the user feedback that the Gemini analysis is running.
The form component is ready, but it needs an App component to render it on the page.
- Add the following App component below the FeedbackForm function in the same file:
function App() {
const [feedbackList, setFeedbackList] = useState([]);
// Add new feedback to the top of the list
const handleNewFeedback = (entry) => {
setFeedbackList((prev) => [entry, ...prev]);
};
return (
<div className="app">
<h1>Customer Feedback Analyzer</h1>
<FeedbackForm onSubmit={handleNewFeedback} />
</div>
);
}
export default App;
What does this code do?
The App component holds the feedbackList state, which stores all the feedback entries. The handleNewFeedback function prepends new entries to the top of the list so the most recent feedback appears first.
- Save App.jsx.
- Switch back to your browser at http://localhost:5173. You should see a heading that says "Customer Feedback Analyzer" and an unstyled form with two input fields and a submit button.
- Type a name and some feedback text, then click the submit button. The button text should briefly change to "Analyzing with AI..." before resetting.
How do I know the submission worked?
After submitting, the form fields clear. You can also check your Express server terminal. You should see the AI analysis request happening. The entry is stored in the backend, but you cannot see it in the browser yet because you have not built the feedback list component.
Form not showing up?
- Make sure your Vite dev server is still running in the client/ folder. If it stopped, run npm run dev again.
- Check the browser console for errors. A missing import or a typo in the component name will prevent rendering.
- If you see a network error when submitting, confirm your Express server is still running on port 3001.
Help me debug why my React form isn't rendering or submitting correctly.
Build the feedback list with AI insights
Each feedback entry comes back from the API with sentiment analysis, extracted themes, and a suggested response. You need a card component that displays all of this information, plus edit and delete buttons for managing entries.
- In App.jsx, find the closing brace of the FeedbackForm function. Add the following FeedbackCard component below it, before the App function:
function FeedbackCard({ entry, onDelete, onEdit }) {
const [editing, setEditing] = useState(false);
const [editName, setEditName] = useState(entry.customerName);
const [editText, setEditText] = useState(entry.feedbackText);
// Map sentiment values to colors
const sentimentColor = {
positive: '#4caf50',
neutral: '#ff9800',
negative: '#f44336',
unknown: '#9e9e9e',
};
// Send updated feedback to the backend
const handleSave = async () => {
try {
const res = await fetch(`${API_URL}/${entry.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customerName: editName, feedbackText: editText }),
});
const updated = await res.json();
onEdit(updated);
setEditing(false);
} catch (error) {
console.error('Failed to update feedback:', error);
}
};
// Remove this entry from the backend
const handleDelete = async () => {
try {
await fetch(`${API_URL}/${entry.id}`, { method: 'DELETE' });
onDelete(entry.id);
} catch (error) {
console.error('Failed to delete feedback:', error);
}
};
return (
<div className="feedback-card">
<div className="card-header">
<span
className="sentiment-badge"
style={{ backgroundColor: sentimentColor[entry.sentiment] }}
>
{entry.sentiment}
</span>
<div className="card-actions">
<button onClick={() => setEditing(!editing)}>
{editing ? 'Cancel' : 'Edit'}
</button>
<button className="delete-btn" onClick={handleDelete}>Delete</button>
</div>
</div>
{editing ? (
<div className="edit-form">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
/>
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
rows={3}
/>
<button onClick={handleSave}>Save</button>
</div>
) : (
<>
<h3>{entry.customerName}</h3>
<p className="feedback-text">{entry.feedbackText}</p>
{entry.themes.length > 0 && (
<div className="themes">
{entry.themes.map((theme, i) => (
<span key={i} className="theme-tag">{theme}</span>
))}
</div>
)}
{entry.suggestedResponse && (
<div className="suggested-response">
<strong>Suggested Response:</strong>
<p>{entry.suggestedResponse}</p>
</div>
)}
</>
)}
</div>
);
}
What does this code do?
- sentimentColor maps each sentiment value to a color. Green for positive, yellow for neutral, red for negative, and grey for unknown.
- handleSave sends a PUT request to update the entry's name and text on the backend.
- handleDelete sends a DELETE request and tells the parent component to remove the entry from its list.
- The card conditionally renders either an edit form (with input fields and a Save button) or the read-only display showing sentiment badges, theme tags, and the suggested response.
Now you need to update the App component to fetch existing feedback on page load, handle deletions and edits, and render each entry using FeedbackCard.
- In App.jsx, find the existing App function. Replace the entire function (from function App() down to export default App;) with this updated version:
function App() {
const [feedbackList, setFeedbackList] = useState([]);
// Fetch all existing feedback when the page loads
useEffect(() => {
fetch(API_URL)
.then((res) => res.json())
.then((data) => setFeedbackList(data))
.catch((error) => console.error('Failed to fetch feedback:', error));
}, []);
// Add new feedback to the top of the list
const handleNewFeedback = (entry) => {
setFeedbackList((prev) => [entry, ...prev]);
};
// Remove a deleted entry from state
const handleDelete = (id) => {
setFeedbackList((prev) => prev.filter((e) => e.id !== id));
};
// Replace the edited entry in state
const handleEdit = (updated) => {
setFeedbackList((prev) =>
prev.map((e) => (e.id === updated.id ? updated : e))
);
};
return (
<div className="app">
<h1>Customer Feedback Analyzer</h1>
<FeedbackForm onSubmit={handleNewFeedback} />
<div className="feedback-list">
{feedbackList.length === 0 ? (
<p className="empty-state">No feedback yet. Submit some above!</p>
) : (
feedbackList.map((entry) => (
<FeedbackCard
key={entry.id}
entry={entry}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))
)}
</div>
</div>
);
}
export default App;
What does this code do?
- useEffect with an empty dependency array runs once when the page loads, fetching any previously submitted feedback from the API.
- handleDelete filters the entry out of state. handleEdit swaps the old entry for the updated one using map.
- When the list is empty, a placeholder message appears. Otherwise, each entry renders as a FeedbackCard with delete and edit callbacks wired up.
- Save App.jsx and switch back to your browser.
- You should see the "No feedback yet" message. Submit a new feedback entry using the form. After the AI finishes analyzing, a card should appear showing the customer name, feedback text, a sentiment label (positive, neutral, or negative), theme tags, and a suggested response.
- Click the **Edit** button on a card, change the text, and click **Save**. The card should update.
- Click the **Delete** button. The card should disappear.
Cards not showing up after submitting?
- Open your browser's developer console and check for errors. A common cause is a typo in the FeedbackCard component name or a missing import.
- Make sure FeedbackCard is defined between FeedbackForm and App in the file. The App component must be able to reference it.
- Check that your Express server is still running. If it crashed, restart it from the server/ folder with npm start.
Help me debug why my feedback cards aren't appearing after form submission.
✔️ Awesome, I've got everything!
Great. Double check you have saved App.jsx before moving on to styling.
ⓧ I'd like to double check the full code
import { useState, useEffect } from 'react';
import './App.css';
const API_URL = 'http://localhost:3001/api/feedback';
function FeedbackForm({ onSubmit }) {
const [customerName, setCustomerName] = useState('');
const [feedbackText, setFeedbackText] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!customerName.trim() || !feedbackText.trim()) return;
setLoading(true);
try {
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customerName, feedbackText }),
});
const data = await res.json();
onSubmit(data);
setCustomerName('');
setFeedbackText('');
} catch (error) {
console.error('Failed to submit feedback:', error);
} finally {
setLoading(false);
}
};
return (
<form className="feedback-form" onSubmit={handleSubmit}>
<h2>Submit Feedback</h2>
<input
type="text"
placeholder="Customer Name"
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
required
/>
<textarea
placeholder="Enter customer feedback..."
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
rows={4}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Analyzing with AI...' : 'Submit Feedback'}
</button>
</form>
);
}
function FeedbackCard({ entry, onDelete, onEdit }) {
const [editing, setEditing] = useState(false);
const [editName, setEditName] = useState(entry.customerName);
const [editText, setEditText] = useState(entry.feedbackText);
const sentimentColor = {
positive: '#4caf50',
neutral: '#ff9800',
negative: '#f44336',
unknown: '#9e9e9e',
};
const handleSave = async () => {
try {
const res = await fetch(`${API_URL}/${entry.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customerName: editName, feedbackText: editText }),
});
const updated = await res.json();
onEdit(updated);
setEditing(false);
} catch (error) {
console.error('Failed to update feedback:', error);
}
};
const handleDelete = async () => {
try {
await fetch(`${API_URL}/${entry.id}`, { method: 'DELETE' });
onDelete(entry.id);
} catch (error) {
console.error('Failed to delete feedback:', error);
}
};
return (
<div className="feedback-card">
<div className="card-header">
<span
className="sentiment-badge"
style={{ backgroundColor: sentimentColor[entry.sentiment] }}
>
{entry.sentiment}
</span>
<div className="card-actions">
<button onClick={() => setEditing(!editing)}>
{editing ? 'Cancel' : 'Edit'}
</button>
<button className="delete-btn" onClick={handleDelete}>Delete</button>
</div>
</div>
{editing ? (
<div className="edit-form">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
/>
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
rows={3}
/>
<button onClick={handleSave}>Save</button>
</div>
) : (
<>
<h3>{entry.customerName}</h3>
<p className="feedback-text">{entry.feedbackText}</p>
{entry.themes.length > 0 && (
<div className="themes">
{entry.themes.map((theme, i) => (
<span key={i} className="theme-tag">{theme}</span>
))}
</div>
)}
{entry.suggestedResponse && (
<div className="suggested-response">
<strong>Suggested Response:</strong>
<p>{entry.suggestedResponse}</p>
</div>
)}
</>
)}
</div>
);
}
function App() {
const [feedbackList, setFeedbackList] = useState([]);
useEffect(() => {
fetch(API_URL)
.then((res) => res.json())
.then((data) => setFeedbackList(data))
.catch((error) => console.error('Failed to fetch feedback:', error));
}, []);
const handleNewFeedback = (entry) => {
setFeedbackList((prev) => [entry, ...prev]);
};
const handleDelete = (id) => {
setFeedbackList((prev) => prev.filter((e) => e.id !== id));
};
const handleEdit = (updated) => {
setFeedbackList((prev) =>
prev.map((e) => (e.id === updated.id ? updated : e))
);
};
return (
<div className="app">
<h1>Customer Feedback Analyzer</h1>
<FeedbackForm onSubmit={handleNewFeedback} />
<div className="feedback-list">
{feedbackList.length === 0 ? (
<p className="empty-state">No feedback yet. Submit some above!</p>
) : (
feedbackList.map((entry) => (
<FeedbackCard
key={entry.id}
entry={entry}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))
)}
</div>
</div>
);
}
export default App;
Add basic styling
The app is fully functional but looks rough without any styling. A clean layout with card-based entries and color-coded sentiment badges makes the data much easier to read at a glance.
- Open client/src/App.css and replace any existing content with the following global and form styles:
/* Reset browser defaults */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Base body styling */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
}
/* Centered container */
.app {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
}
h1 {
text-align: center;
margin-bottom: 2rem;
color: #1a1a2e;
}
/* Feedback form card */
.feedback-form {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.feedback-form h2 {
margin-bottom: 1rem;
}
/* Form inputs */
.feedback-form input,
.feedback-form textarea {
width: 100%;
padding: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
/* Submit button */
.feedback-form button {
width: 100%;
padding: 0.75rem;
background-color: #1a1a2e;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
/* Disabled state while AI is processing */
.feedback-form button:disabled {
background-color: #999;
cursor: not-allowed;
}
What does this code do?
The CSS reset removes default browser margins and padding so the layout starts from a clean slate. The .app container centers everything at a max width of 800px.
The form gets a white card look with rounded corners and a subtle shadow. The submit button turns grey and disables the cursor when the AI is processing.
- Add the following styles below the form styles in the same file. These handle the feedback cards, sentiment badges, theme tags, and edit form:
/* Feedback list layout */
.feedback-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.empty-state {
text-align: center;
color: #999;
padding: 2rem;
}
/* Individual feedback card */
.feedback-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
/* Color-coded sentiment pill */
.sentiment-badge {
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
text-transform: capitalize;
}
/* Edit and delete buttons */
.card-actions button {
margin-left: 0.5rem;
padding: 0.25rem 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 0.85rem;
}
.delete-btn {
color: #f44336;
border-color: #f44336;
}
.feedback-text {
margin-bottom: 0.75rem;
line-height: 1.5;
}
/* Theme tag pills */
.themes {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.theme-tag {
background-color: #e3f2fd;
color: #1565c0;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
}
/* AI suggested response block */
.suggested-response {
background-color: #f9f9f9;
padding: 0.75rem;
border-radius: 4px;
border-left: 3px solid #1a1a2e;
}
.suggested-response p {
margin-top: 0.25rem;
line-height: 1.5;
}
/* Inline edit form */
.edit-form input,
.edit-form textarea {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.edit-form button {
padding: 0.5rem 1rem;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
What does this code do?
- sentiment-badge is a rounded pill that gets its background color dynamically from the JSX. Green for positive, yellow for neutral, and red for negative.
- theme-tag styles each extracted theme as a small blue tag.
- suggested-response gets a left border accent to visually separate the AI-generated reply from the rest of the card.
- The edit form and delete button styles match the card layout so inline editing feels seamless.
- Save App.css and switch back to your browser.
- You should see a centered layout with a white form card at the top. Submit a feedback entry and you should see a styled card below the form with a colored sentiment badge, blue theme tags, and the suggested response in a grey block with a dark left border.
Styles not applying?
- Make sure the class names in your CSS match the class names in your JSX exactly. For example, feedback-card in CSS must match className="feedback-card" in JSX.
- Confirm that import './App.css' is at the top of your App.jsx file.
- Try a hard refresh in the browser with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear any cached styles.
Help me figure out why my CSS styles aren't showing up in my React app.
✔️ Awesome, I've got everything!
Great. Make sure both App.jsx and App.css are saved.
ⓧ I'd like to double check the full code
Here is the complete App.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.app {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
}
h1 {
text-align: center;
margin-bottom: 2rem;
color: #1a1a2e;
}
.feedback-form {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.feedback-form h2 {
margin-bottom: 1rem;
}
.feedback-form input,
.feedback-form textarea {
width: 100%;
padding: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.feedback-form button {
width: 100%;
padding: 0.75rem;
background-color: #1a1a2e;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
.feedback-form button:disabled {
background-color: #999;
cursor: not-allowed;
}
.feedback-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.empty-state {
text-align: center;
color: #999;
padding: 2rem;
}
.feedback-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.sentiment-badge {
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
text-transform: capitalize;
}
.card-actions button {
margin-left: 0.5rem;
padding: 0.25rem 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 0.85rem;
}
.delete-btn {
color: #f44336;
border-color: #f44336;
}
.feedback-text {
margin-bottom: 0.75rem;
line-height: 1.5;
}
.themes {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.theme-tag {
background-color: #e3f2fd;
color: #1565c0;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
}
.suggested-response {
background-color: #f9f9f9;
padding: 0.75rem;
border-radius: 4px;
border-left: 3px solid #1a1a2e;
}
.suggested-response p {
margin-top: 0.25rem;
line-height: 1.5;
}
.edit-form input,
.edit-form textarea {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.edit-form button {
padding: 0.5rem 1rem;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
Secret mission
Add a Sentiment Dashboard
Your feedback analyzer collects AI-powered insights on every submission. But what if you could see the big picture at a glance? In this secret mission, build a sentiment dashboard that visualizes the breakdown of positive, neutral, and negative feedback across all entries, plus the most common themes.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally with a free-tier API key, so there are no ongoing costs.
Resources you used:
- The feedback-analyzer project folder on your Desktop (containing client/ and server/ directories).
- A Google AI Studio API key for the Gemini API.
- Running processes: the Vite dev server and the Express backend server.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your feedback analyzer.
- Your Vite dev server and Express server will keep running until you close the terminal windows.
- Your project files stay on your Desktop.
- Your Gemini API key remains active in Google AI Studio. The free tier has no expiration, so there is nothing to worry about.
Pause - I'll come back to this later
Shut down the running servers to free up terminal windows and memory. Your files and API key stay intact for when you return.
- Switch to the terminal running your Express server and press Ctrl+C to stop it.
- Switch to the terminal running your Vite dev server and press Ctrl+C to stop it.
- Your project files and API key are preserved. When you are ready to continue, run npm start in the server/ folder and npm run dev in the client/ folder to pick up where you left off.
Delete - I don't want to use this again
Remove all project files and revoke your API key to clean up completely.
- Stop both running servers by pressing Ctrl+C in each terminal window.
- Delete the project folder on macOS/Linux by running:
cd ~/Desktop
rm -rf feedback-analyzer
- On Windows, run:
cd ~\Desktop
rmdir /s /q feedback-analyzer
- To revoke your Gemini API key, go to https://aistudio.google.com and open the API Keys section in the left sidebar. Find the key you created for this project and delete it.
Nice Work!
Nice Work!
You just built a full-stack customer feedback analyzer powered by AI. From setting up an Express server to wiring up Gemini for real-time sentiment analysis, this project covered serious ground.
You've learned how to:
- Built a full-stack CRUD application with a React frontend and Node.js/Express backend.
- Integrated the Google Gemini AI API to perform sentiment analysis, theme extraction, and response generation on customer feedback.
- Implemented all four CRUD operations (Create, Read, Update, Delete) with a REST API.
- Secret Mission: Added a dynamic sentiment dashboard with visual breakdown statistics that update in real-time.
Ready to quiz yourself?