Build an AI Lunch Decider App
Build a web app that uses Gemini AI to suggest lunch from your ingredients.
Introduction
30 Second Summary
It's lunchtime and you're staring into the fridge, completely unable to decide what to eat. What if you could type in whatever you have on hand and let AI make the call for you?
In this project, you will build a "What Should I Eat?" web app that takes your available ingredients and uses Gemini (an AI model) to suggest a specific lunch. You'll wire together a frontend, a server, and an AI API into a working tool you can actually use.
What You'll Build
A locally-running web app powered by a Node.js server that calls the Gemini API to generate opinionated lunch suggestions based on your ingredients.
By the end of this project, you'll have:
- A Node.js/Express backend server that sends your ingredients to Gemini and returns a lunch suggestion.
- An HTML/CSS/JS frontend with a text input for ingredients and a display area for Gemini's response.
- A system prompt that gives Gemini a fun, decisive personality as your lunch advisor.
- Secret Mission: Add a "Surprise Me" button that generates a wildcard suggestion with a random cuisine constraint.
Are there any prerequisites?
You should have basic familiarity with HTML, CSS, and JavaScript. No Node.js or API experience is needed. This project walks you through everything from scratch.
Before We Start
Before We Start...
Set Up Your Project
Before building anything, you need the right tools in place.
In this step, you will install Node.js (the runtime that lets you run JavaScript outside a browser), create a Google AI Studio API key so you can call the Gemini API, and scaffold your project folder with the right dependencies.
In this step, get ready to:
- Install Node.js and verify it works.
- Create a Google AI Studio API key.
- Scaffold a project folder with your dependencies and environment file.
Install Node.js
Node.js lets you run JavaScript on your computer (outside of a web browser). You need it to create your server and install packages.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check if Node.js is already installed by running this command:
node -v
✔️ I see version 24 or higher
You are good to go. Confirm npm is also available by running:
npm -v
You should see a version number printed. Move on to the next substep.
ⓧ I see an older version
You need version 24 or higher for this project. Download the latest LTS installer from the official site:
- Go to https://nodejs.org/en/download in your browser.
- Download the LTS installer for your operating system (.pkg for macOS, .msi for Windows).
- Run the installer and accept the default settings.
- Close and reopen your terminal, then run node -v again to confirm it now shows v24 or higher.
Still seeing the old version?
On macOS, an older Node.js may remain in your path. Close all terminal windows and open a fresh one. If the version still shows the old number, you may need to uninstall the old version first or use a version manager like nvm.
help me upgrade Node.js to version 24
ⓧ Command not found
Node.js is not installed yet. Download and install it now:
- Go to https://nodejs.org/en/download in your browser.
- Download the LTS installer (.pkg for macOS, .msi for Windows).
- Run the installer and accept the default settings.
- Close and reopen your terminal so the new installation is recognized.
- Run node -v to confirm installation. You should see v24 or higher.
- Run npm -v to confirm npm is also available.
Installer not working?
Make sure you downloaded the correct file for your operating system. On macOS, look for the .pkg file. On Windows, look for the .msi file.
help me install Node.js on my computer
Get a free Gemini API key
Your app needs to talk to Gemini's AI. To do that, you need an API key that identifies your account when making requests. The Gemini API is free to use with your Google account.
- Go to aistudio.google.com in your browser.
- Sign in with your Google account.
- Click Get API Key in the left sidebar.
- Click Create API Key.
- Copy the key immediately and paste it somewhere safe (like a notes app). You will need it in a moment.
What is an API key?
An API key is like a password that lets your code access a service. In this case, it proves to Google that you have permission to use Gemini.
Never share your key publicly or commit it to version control. You will store it in a hidden .env file so it stays private.
💡 Is the Gemini API really free?
The Gemini API free tier gives you free input and output tokens with no credit card required. If you ever exceed the free quota, requests simply fail with an error. You will never be charged.
Scaffold the project
Now you will create your project folder, initialize it with npm, and install the three packages your app needs.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir lunch-decider
cd lunch-decider
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the lunch-decider folder, then click Open.
- Open a terminal inside VS Code by pressing Ctrl+` (backtick key, top left of keyboard). This is the terminal you will use for the rest of the project.
Why use the VS Code terminal?
The integrated terminal automatically opens in your project folder. Any commands you run here will apply to the lunch-decider directory without needing to navigate again.
- Initialize a new Node.js project by running this command:
npm init -y
What does this command do?
The npm init -y command creates a package.json file with default values. This file tracks your project's name, version, and dependencies.
- Install the three packages your project needs by running this command:
npm install express @google/genai dotenv
What are these packages?
- express is a web framework that creates your server and handles incoming requests.
- @google/genai is Google's official library for calling the Gemini API from JavaScript.
- dotenv loads environment variables from a .env file so your API key stays private.
You should see output ending with a summary of packages added. A node_modules folder and package-lock.json file now appear in your project.
Seeing permission errors or install failures?
Make sure your terminal is inside the lunch-decider folder. Run pwd (macOS) or cd (Windows) to check your current directory. It should end with lunch-decider.
help me fix npm install errors
- Open package.json in VS Code's Explorer sidebar (left panel) and replace its entire contents with this:
{
"name": "lunch-decider",
"version": "1.0.0",
"description": "AI-powered lunch suggestion app",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@google/genai": "1.0.1",
"dotenv": "17.4.2",
"express": "4.19.2"
}
}
Why replace the file?
The default npm init generates extra fields you do not need. This cleaned-up version sets main to server.js (the file you will create next step) and adds a start script so you can launch your app with npm start.
- Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Create the environment file by running this command in your VS Code terminal:
touch .env
- Open .env in VS Code and paste this line:
GEMINI_API_KEY=your-api-key-here
- Replace your-api-key-here with the API key you copied from Google AI Studio earlier.
- Save .env by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Keep your API key out of version control
If you ever push this project to GitHub, add .env to a .gitignore file so your key does not get published.
- Confirm your project structure by running:
ls
You should see .env, node_modules, package-lock.json, and package.json listed in the output.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files.
ⓧ I'd like to double check the full code
Here is the exact content each file should have at this point:
{
"name": "lunch-decider",
"version": "1.0.0",
"description": "AI-powered lunch suggestion app",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@google/genai": "1.0.1",
"dotenv": "17.4.2",
"express": "4.19.2"
}
}
GEMINI_API_KEY=your-api-key-here
The .env file should contain your actual API key in place of your-api-key-here.
Your environment is ready. Next up, you will build the server that talks to Gemini and returns lunch suggestions.
Build the Server
Your project folder is ready with all its dependencies installed. Now it's time to build the brain of your app.
The server is what connects your frontend (the page the user sees) to the Gemini API (the AI that generates lunch suggestions). Your frontend can't call the Gemini API directly because of security restrictions.
Why do we need a server?
The Gemini API blocks direct calls from browsers due to CORS restrictions. It also requires an API key that should never be exposed in frontend code.
A server acts as a secure middleman. The browser talks to your server, your server talks to Gemini, and your API key stays hidden. This is the standard pattern behind every AI-powered web app.
In this step, get ready to:
- Create server.js with Express and middleware.
- Wire up the Gemini API to suggest lunches.
- Test the server runs on port 3000.
Set up Express and the Gemini client
Your server needs two jobs: serve your webpage files and handle API requests. Express handles both with just a few lines of configuration.
- Create a new file called server.js in the root of your lunch-decider folder by running this command in your terminal:
touch server.js
- Confirm the file exists by running:
ls
You should see server.js listed alongside package.json and your .env file.
- Open server.js in the editor by clicking on it in the Explorer sidebar on the left.
- Add the server setup code by copying the following into server.js:
// Import the libraries we installed
const express = require('express');
const { GoogleGenAI } = require('@google/genai');
const path = require('path');
// Load environment variables from .env file
require('dotenv').config();
// Create the Express app and set the port
const app = express();
const port = 3000;
// Middleware: parse JSON request bodies and serve static files
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// Create the Gemini client using your API key
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
What does this code do?
- require('express') imports the Express web framework to handle HTTP requests.
- require('dotenv').config() loads your .env file so process.env.GEMINI_API_KEY has your key.
- express.json() lets your server read JSON data sent from the browser.
- express.static() serves your HTML, CSS, and JS files from a public folder automatically.
- new GoogleGenAI() creates a client that can talk to the Gemini API using your key.
- Save server.js.
You won't see any output yet. The next section adds the endpoint that makes the server useful.
Seeing a red underline in your editor?
If VS Code shows a warning on the require lines, that's normal. It expects ES module syntax by default but Node.js CommonJS works fine.
help me understand why VS Code is showing warnings on my require statements
Create the lunch suggestion endpoint
This is where the magic happens. You'll create a POST endpoint that receives ingredients from the browser, sends them to Gemini with a fun personality prompt, and returns the suggestion.
- Add the following code below the Gemini client in server.js:
// POST endpoint that receives ingredients and returns a lunch suggestion
app.post('/api/suggest', async (req, res) => {
const { ingredients, surprise } = req.body;
// Validate that the user sent ingredients
if (!ingredients || ingredients.trim() === '') {
return res.status(400).json({ error: 'Please tell me what you have available!' });
}
// Build the system prompt that gives Gemini its personality
let systemPrompt = `You are a fun, opinionated lunch advisor. When someone tells you what ingredients or food options they have available, you suggest ONE specific lunch option and explain why it's the best choice. Be enthusiastic and decisive - no wishy-washy "you could try" nonsense. Tell them exactly what to eat and why. Keep your response to 2-3 short paragraphs.`;
// If surprise mode is active, add a random cuisine constraint
if (surprise) {
const cuisines = ['Thai', 'Italian', 'Mexican', 'Japanese', 'Indian', 'Mediterranean', 'Korean', 'French', 'Ethiopian', 'Peruvian'];
const randomCuisine = cuisines[Math.floor(Math.random() * cuisines.length)];
systemPrompt += ` You MUST suggest something inspired by ${randomCuisine} cuisine. Get creative with how the available ingredients could work in a ${randomCuisine}-inspired dish.`;
}
try {
// Send the ingredients to Gemini and get a suggestion
const interaction = await ai.interactions.create({
model: 'gemini-2.0-flash',
input: [
{ type: 'text', text: systemPrompt },
{ type: 'text', text: `Here's what I have available: ${ingredients}. What should I eat for lunch?` },
],
});
// Extract the text response and send it back
const suggestion = interaction.output_text;
res.json({ suggestion });
} catch (error) {
console.error('Error calling Gemini:', error.message);
res.status(500).json({ error: 'Something went wrong getting your lunch suggestion. Try again!' });
}
});
// Start the server
app.listen(port, () => {
console.log(`Lunch Decider is running at http://localhost:${port}`);
});
What does this endpoint do?
- It reads ingredients and surprise from the request body sent by the browser.
- It validates that the user actually typed something before calling the API.
- The systemPrompt gives Gemini a specific personality. It tells Gemini to be decisive and opinionated, not wishy-washy.
- If surprise mode is on, it picks a random cuisine from the array and adds that constraint to the prompt.
- ai.interactions.create() sends the request to the Gemini Flash model and waits for a response.
- The try/catch block handles errors gracefully so the user sees a friendly message instead of a crash.
💡 What is a system prompt?
A system prompt is a set of instructions that tells the AI how to behave. It's separate from the user's message. Think of it as giving the AI a job description before it starts working.
In this project, the system prompt turns Gemini into a fun, opinionated lunch advisor. Without it, Gemini would give generic, balanced responses. With it, Gemini commits to a single bold suggestion.
- Save server.js.
Seeing a syntax error highlighted?
Template literals (the strings using backtick characters) can trip up copy-paste. Make sure the backtick characters around the system prompt and the console.log line are actual backticks, not regular single quotes.
Also check that the }); at the end of app.post and app.listen both have matching opening brackets.
help me find the syntax error in my server.js file
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js before moving on to the test.
ⓧ I'd like to double check the full code
Here's the complete server.js file. Make sure yours matches exactly:
const express = require('express');
const { GoogleGenAI } = require('@google/genai');
const path = require('path');
require('dotenv').config();
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
app.post('/api/suggest', async (req, res) => {
const { ingredients, surprise } = req.body;
if (!ingredients || ingredients.trim() === '') {
return res.status(400).json({ error: 'Please tell me what you have available!' });
}
let systemPrompt = `You are a fun, opinionated lunch advisor. When someone tells you what ingredients or food options they have available, you suggest ONE specific lunch option and explain why it's the best choice. Be enthusiastic and decisive - no wishy-washy "you could try" nonsense. Tell them exactly what to eat and why. Keep your response to 2-3 short paragraphs.`;
if (surprise) {
const cuisines = ['Thai', 'Italian', 'Mexican', 'Japanese', 'Indian', 'Mediterranean', 'Korean', 'French', 'Ethiopian', 'Peruvian'];
const randomCuisine = cuisines[Math.floor(Math.random() * cuisines.length)];
systemPrompt += ` You MUST suggest something inspired by ${randomCuisine} cuisine. Get creative with how the available ingredients could work in a ${randomCuisine}-inspired dish.`;
}
try {
const interaction = await ai.interactions.create({
model: 'gemini-2.0-flash',
input: [
{ type: 'text', text: systemPrompt },
{ type: 'text', text: `Here's what I have available: ${ingredients}. What should I eat for lunch?` },
],
});
const suggestion = interaction.output_text;
res.json({ suggestion });
} catch (error) {
console.error('Error calling Gemini:', error.message);
res.status(500).json({ error: 'Something went wrong getting your lunch suggestion. Try again!' });
}
});
app.listen(port, () => {
console.log(`Lunch Decider is running at http://localhost:${port}`);
});
Test the server
Your server file is complete. Time to start it up and confirm everything loads without errors.
- Start the server by running this command in your terminal:
node server.js
You should see this message in your terminal:
Lunch Decider is running at http://localhost:3000
What should I see?
The message confirms Express started successfully and is listening for requests on port 3000. Your server is now running, but there's nothing to see in the browser yet because you haven't built the frontend.
If you visit http://localhost:3000 right now, you'll see an error page. That's expected. The public folder doesn't exist yet.
🙋♀️ Seeing an error instead?
If you see Cannot find module 'express', your dependencies didn't install properly. Stop the server with Ctrl+C and run npm install again.
If you see a different error about GEMINI_API_KEY, check that your .env file is in the lunch-decider folder (not inside a subfolder) and contains your key.
help me debug why my Node.js server won't start
- Once you've confirmed the server is running, press Ctrl+C in the terminal to stop it.
Your server is built and tested. Next up, you'll create the frontend that users actually interact with.
Build the Frontend
Your server is ready to receive requests and talk to Claude. Now it needs a face.
In this step, you will build the frontend that your users actually see and interact with. That means an HTML page with a text area, a stylesheet to make it look inviting, and a JavaScript file that sends the user's input to your server and displays Claude's lunch suggestion.
In this step, get ready to:
- Create an HTML page with an input area and buttons.
- Style the page with CSS for a clean, food-themed design.
- Write the JavaScript that connects your frontend to the server.
Create the HTML page
Your server already serves static files from a public folder (that's what the express.static line in server.js does). You just need to create that folder and put your frontend files inside it.
- Create the public folder by running this command in your terminal:
mkdir public
- Confirm the folder was created by running:
ls
You should see public listed alongside server.js, package.json, and your other project files.
- In VS Code's Explorer sidebar, right-click the public folder and select New File.
- Name the file index.html and press Enter.
- Paste the following HTML into public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>What Should I Eat?</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>What Should I Eat? 🍽️</h1>
<p class="subtitle">Type what's in your fridge or nearby, and I'll decide for you.</p>
<textarea id="ingredients" placeholder="e.g. chicken, rice, soy sauce, broccoli, leftover pasta, that random can of beans..."></textarea>
<div class="buttons">
<button id="suggest-btn" onclick="getSuggestion(false)">What Should I Eat?</button>
<button id="surprise-btn" onclick="getSuggestion(true)">Surprise Me! 🎲</button>
</div>
<div id="loading" class="loading hidden">Thinking about your lunch...</div>
<div id="result" class="result hidden"></div>
</div>
<script src="script.js"></script>
</body>
</html>
What does this HTML do?
- The <textarea> gives users a large input area to type their available ingredients.
- Two buttons call the same getSuggestion() function but with different arguments. false means a normal suggestion. true triggers a surprise cuisine mode.
- The loading and result divs start hidden (via the hidden class) and get revealed by JavaScript when needed.
- The <link> tag loads style.css and the <script> tag loads script.js. You will create both of these next.
- Save public/index.html with Cmd+S (macOS) or Ctrl+S (Windows).
You won't see anything in the browser yet because the stylesheet and script files don't exist. The next chunk creates the styles.
File not appearing in the public folder?
- Make sure you right-clicked the public folder specifically, not the root lunch-decider folder. The file must be inside public/.
- Check that you ran mkdir public from inside the lunch-decider folder. If the folder doesn't show in Explorer, try clicking the refresh icon at the top of the Explorer panel.
- Help me fix this
Style the page with CSS
A plain HTML page works but isn't inviting. A short stylesheet transforms it into a warm, card-style layout that feels like a friendly app.
- Right-click the public folder in VS Code's Explorer and select New File.
- Name it style.css and press Enter.
- Paste the following CSS into public/style.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #fef9f0;
color: #333;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
max-width: 600px;
width: 100%;
background: white;
border-radius: 16px;
padding: 40px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
h1 {
font-size: 2rem;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 24px;
}
textarea {
width: 100%;
height: 120px;
padding: 16px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 1rem;
font-family: inherit;
resize: vertical;
transition: border-color 0.2s;
}
textarea:focus {
outline: none;
border-color: #f59e0b;
}
.buttons {
display: flex;
gap: 12px;
margin-top: 16px;
}
button {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.1s, opacity 0.2s;
}
button:active {
transform: scale(0.97);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
#suggest-btn {
background-color: #f59e0b;
color: white;
}
#surprise-btn {
background-color: #8b5cf6;
color: white;
}
.loading {
text-align: center;
margin-top: 24px;
color: #666;
font-style: italic;
}
.result {
margin-top: 24px;
padding: 24px;
background-color: #f0fdf4;
border-radius: 12px;
border: 1px solid #bbf7d0;
line-height: 1.6;
white-space: pre-wrap;
}
.hidden {
display: none;
}
What do these styles do?
- The warm #fef9f0 background and white card with rounded corners create a friendly, food-app vibe.
- The amber #suggest-btn and purple #surprise-btn give each button a distinct personality so users can tell them apart at a glance.
- The .hidden class uses display: none to hide the loading message and result area until JavaScript reveals them.
- The button:disabled style dims buttons while waiting for the API response, preventing double-clicks.
- Save public/style.css.
You won't see the styled page until you start the server in the next step. The next file you create will handle the button logic.
Worried the CSS isn't working?
- Double-check that style.css is inside the public folder, not in the root lunch-decider folder. The HTML file references it with a relative path, so both files must be in the same directory.
- Make sure the filename is exactly style.css (not styles.css or Style.css). It must match the <link> tag in your HTML.
- Help me troubleshoot my CSS
Add the frontend JavaScript
The JavaScript file is the bridge between the user's input and your server. It reads the ingredients, sends them to /api/suggest, and displays Claude's response on the page.
- Right-click the public folder and select New File.
- Name it script.js and press Enter.
- Paste the following JavaScript into public/script.js:
async function getSuggestion(surprise) {
const ingredients = document.getElementById('ingredients').value;
const resultDiv = document.getElementById('result');
const loadingDiv = document.getElementById('loading');
const suggestBtn = document.getElementById('suggest-btn');
const surpriseBtn = document.getElementById('surprise-btn');
if (!ingredients.trim()) {
resultDiv.textContent = 'Tell me what you have available first!';
resultDiv.classList.remove('hidden');
return;
}
suggestBtn.disabled = true;
surpriseBtn.disabled = true;
resultDiv.classList.add('hidden');
loadingDiv.classList.remove('hidden');
try {
const response = await fetch('/api/suggest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ingredients, surprise }),
});
const data = await response.json();
if (response.ok) {
resultDiv.textContent = data.suggestion;
} else {
resultDiv.textContent = data.error || 'Something went wrong. Try again!';
}
} catch (error) {
resultDiv.textContent = 'Could not connect to the server. Is it running?';
}
loadingDiv.classList.add('hidden');
resultDiv.classList.remove('hidden');
suggestBtn.disabled = false;
surpriseBtn.disabled = false;
}
What does this code do?
- The function grabs references to every element it needs (textarea, buttons, loading indicator, result area) so it can control them all.
- If the textarea is empty, it shows a friendly reminder instead of hitting the server with a blank request.
- Both buttons get disabled while waiting for the API response. This prevents the user from spamming requests.
- The fetch call sends a POST request to /api/suggest with the ingredients and the surprise flag as JSON. The server handles the rest.
- The catch block handles network errors gracefully, showing a message if the server isn't reachable.
- Save public/script.js.
- Confirm all three frontend files exist by running this in your terminal:
ls public/
You should see index.html, script.js, and style.css listed.
Missing a file in the output?
- If a file is missing from the ls public/ output, it was likely saved to the wrong location. Check the root lunch-decider folder for stray files and move them into public/.
- Make sure each file is named exactly as shown: index.html, style.css, script.js. Capitalisation and typos will break the links between files.
- Help me find my missing files
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files, then continue to the next step where you'll bring the whole app to life.
ⓧ I'd like to double check the full code
Here are all three frontend files for comparison.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>What Should I Eat?</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>What Should I Eat? 🍽️</h1>
<p class="subtitle">Type what's in your fridge or nearby, and I'll decide for you.</p>
<textarea id="ingredients" placeholder="e.g. chicken, rice, soy sauce, broccoli, leftover pasta, that random can of beans..."></textarea>
<div class="buttons">
<button id="suggest-btn" onclick="getSuggestion(false)">What Should I Eat?</button>
<button id="surprise-btn" onclick="getSuggestion(true)">Surprise Me! 🎲</button>
</div>
<div id="loading" class="loading hidden">Thinking about your lunch...</div>
<div id="result" class="result hidden"></div>
</div>
<script src="script.js"></script>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #fef9f0;
color: #333;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
max-width: 600px;
width: 100%;
background: white;
border-radius: 16px;
padding: 40px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
h1 {
font-size: 2rem;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 24px;
}
textarea {
width: 100%;
height: 120px;
padding: 16px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 1rem;
font-family: inherit;
resize: vertical;
transition: border-color 0.2s;
}
textarea:focus {
outline: none;
border-color: #f59e0b;
}
.buttons {
display: flex;
gap: 12px;
margin-top: 16px;
}
button {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.1s, opacity 0.2s;
}
button:active {
transform: scale(0.97);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
#suggest-btn {
background-color: #f59e0b;
color: white;
}
#surprise-btn {
background-color: #8b5cf6;
color: white;
}
.loading {
text-align: center;
margin-top: 24px;
color: #666;
font-style: italic;
}
.result {
margin-top: 24px;
padding: 24px;
background-color: #f0fdf4;
border-radius: 12px;
border: 1px solid #bbf7d0;
line-height: 1.6;
white-space: pre-wrap;
}
.hidden {
display: none;
}
async function getSuggestion(surprise) {
const ingredients = document.getElementById('ingredients').value;
const resultDiv = document.getElementById('result');
const loadingDiv = document.getElementById('loading');
const suggestBtn = document.getElementById('suggest-btn');
const surpriseBtn = document.getElementById('surprise-btn');
if (!ingredients.trim()) {
resultDiv.textContent = 'Tell me what you have available first!';
resultDiv.classList.remove('hidden');
return;
}
suggestBtn.disabled = true;
surpriseBtn.disabled = true;
resultDiv.classList.add('hidden');
loadingDiv.classList.remove('hidden');
try {
const response = await fetch('/api/suggest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ingredients, surprise }),
});
const data = await response.json();
if (response.ok) {
resultDiv.textContent = data.suggestion;
} else {
resultDiv.textContent = data.error || 'Something went wrong. Try again!';
}
} catch (error) {
resultDiv.textContent = 'Could not connect to the server. Is it running?';
}
loadingDiv.classList.add('hidden');
resultDiv.classList.remove('hidden');
suggestBtn.disabled = false;
surpriseBtn.disabled = false;
}
Your frontend is built. In the next step, you will start the server, open the app in your browser, and get your first AI-powered lunch suggestion.
Test and Refine Your App
You have a complete frontend and backend sitting in your project folder. Now it is time to see them work together.
In this step, you will start your server, send real requests to Gemini, and watch your lunch suggestions appear in the browser. You will also tweak the system prompt to see how it changes Gemini's personality.
In this step, get ready to:
- Start the server and get your first lunch suggestion.
- Experiment with the system prompt to change Gemini's personality.
- Verify your app's edge case handling works correctly.
Start the server and get your first lunch suggestion
Your app has two parts that need to connect: the Express server (which talks to Gemini) and the frontend (which the browser displays). Starting the server brings both online at once.
- In VS Code, open the integrated terminal by pressing Ctrl+` (macOS and Windows).
- Start your server by running this command:
node server.js
You should see this message in your terminal:
Lunch Decider is running at http://localhost:3000
- Open your browser and go to http://localhost:3000.
You should see the "What Should I Eat?" page with a text area, a yellow button, and a purple button.
- Type chicken, rice, soy sauce, broccoli into the text area.
- Click the What Should I Eat? button.
After a brief pause, a green box appears below the buttons with Gemini's specific lunch suggestion and an explanation of why it is the best choice.
What just happened?
Your browser sent the ingredients to the Express server at /api/suggest. The server packaged them into a prompt and sent that prompt to the Gemini API. Gemini's response traveled back through the server to your browser.
This frontend-to-server-to-API pattern is how every AI-powered web app works in production. You just built the entire loop from scratch.
🙋♀️ Not seeing a suggestion?
- Check your terminal for error messages. If you see an authentication error, make sure your .env file has a valid API key (not the placeholder text).
- If the page does not load at all, confirm the terminal shows the "Lunch Decider is running" message. If not, check for typos in server.js.
- If you see EADDRINUSE in the terminal, another process is using port 3000. Press Ctrl+C to stop it, close any other terminals running node server.js, and try again.
Still stuck? Help me debug why my server isn't responding to requests.
Experiment with the system prompt
The system prompt is what gives Gemini its personality. Right now, Gemini is a "fun, opinionated lunch advisor." Changing even a few words in the system prompt produces dramatically different responses.
- Stop the server by pressing Ctrl+C in the terminal.
- In server.js, find the line that starts with let systemPrompt.
- Find this text inside the prompt:
Be enthusiastic and decisive - no wishy-washy "you could try" nonsense. Tell them exactly what to eat and why.
- Replace that sentence with something different. Here are two ideas to try:
Prompt variations to try
- Health-focused: "Always prioritize nutrition and balanced macros. Explain the health benefits of your suggestion."
- Dramatic chef: "Respond as if you are a Michelin-star chef who is personally offended by bad lunch choices. Be theatrical and passionate."
- Save server.js.
- Restart the server by running:
node server.js
- Go back to your browser and refresh the page.
- Type the same ingredients and click What Should I Eat? again.
Notice how the same ingredients produce a completely different tone and style of suggestion. This is prompt engineering in action.
What is prompt engineering?
Prompt engineering is the practice of crafting instructions that shape how an LLM responds. Small changes to the system prompt can make Gemini sound professional, playful, strict, or creative.
The system prompt is separate from the user's message. It sets the rules Gemini follows for every conversation, while the user message provides the specific input to respond to.
- When you are done experimenting, change the system prompt back to the original text so your app matches the project code.
- Find the sentence you changed and replace it with:
Be enthusiastic and decisive - no wishy-washy "you could try" nonsense. Tell them exactly what to eat and why.
- Save server.js and restart the server with Ctrl+C followed by:
node server.js
Verify your edge case handling
A working app handles more than just the happy path. Your code already includes protections for common issues. Let's confirm they work.
- In the browser, clear the text area so it is completely empty.
- Click the What Should I Eat? button.
You should see the message "Tell me what you have available first!" appear in the green result area. Your script.js catches the empty input before it even reaches the server.
- Now type some ingredients and click the button again.
- Watch the buttons while the request is loading.
Both buttons turn gray and become unclickable while Gemini is thinking. The text "Thinking about your lunch..." appears below them. Once the response arrives, the buttons re-enable and the loading text disappears.
Why disable buttons during requests?
Without this protection, a user could click the button multiple times while waiting. Each click would fire another API request, wasting credits and potentially showing confusing duplicate responses.
Disabling the buttons and showing a loading indicator gives the user confidence that their request is being processed.
- To test the error state, stop the server by pressing Ctrl+C in the terminal.
- In the browser, type some ingredients and click the button.
You should see the message "Could not connect to the server. Is it running?" in the result area. Your catch block in script.js handles the network failure gracefully instead of showing a cryptic error.
- Start the server again by running:
node server.js
- Confirm everything is working by refreshing the browser and submitting one more suggestion request.
Buttons not re-enabling after an error?
Check your script.js file. The lines that set suggestBtn.disabled = false and surpriseBtn.disabled = false must be outside the try/catch block, at the very end of the function. If they are inside the try block, an error will skip over them.
Still stuck? Help me figure out why my buttons stay disabled after an error.
✔️ Awesome, I've got everything!
Great. Double check you have saved all files and that your server is running.
ⓧ I'd like to double check the full code
Here is the complete script.js file that handles loading, errors, and button disabling:
async function getSuggestion(surprise) {
const ingredients = document.getElementById('ingredients').value;
const resultDiv = document.getElementById('result');
const loadingDiv = document.getElementById('loading');
const suggestBtn = document.getElementById('suggest-btn');
const surpriseBtn = document.getElementById('surprise-btn');
if (!ingredients.trim()) {
resultDiv.textContent = 'Tell me what you have available first!';
resultDiv.classList.remove('hidden');
return;
}
suggestBtn.disabled = true;
surpriseBtn.disabled = true;
resultDiv.classList.add('hidden');
loadingDiv.classList.remove('hidden');
try {
const response = await fetch('/api/suggest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ingredients, surprise }),
});
const data = await response.json();
if (response.ok) {
resultDiv.textContent = data.suggestion;
} else {
resultDiv.textContent = data.error || 'Something went wrong. Try again!';
}
} catch (error) {
resultDiv.textContent = 'Could not connect to the server. Is it running?';
}
loadingDiv.classList.add('hidden');
resultDiv.classList.remove('hidden');
suggestBtn.disabled = false;
surpriseBtn.disabled = false;
}
And here is the complete server.js:
const express = require('express');
const { GoogleGenAI } = require('@google/genai');
const path = require('path');
require('dotenv').config();
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
app.post('/api/suggest', async (req, res) => {
const { ingredients, surprise } = req.body;
if (!ingredients || ingredients.trim() === '') {
return res.status(400).json({ error: 'Please tell me what you have available!' });
}
let systemPrompt = `You are a fun, opinionated lunch advisor. When someone tells you what ingredients or food options they have available, you suggest ONE specific lunch option and explain why it's the best choice. Be enthusiastic and decisive - no wishy-washy "you could try" nonsense. Tell them exactly what to eat and why. Keep your response to 2-3 short paragraphs.`;
if (surprise) {
const cuisines = ['Thai', 'Italian', 'Mexican', 'Japanese', 'Indian', 'Mediterranean', 'Korean', 'French', 'Ethiopian', 'Peruvian'];
const randomCuisine = cuisines[Math.floor(Math.random() * cuisines.length)];
systemPrompt += ` You MUST suggest something inspired by ${randomCuisine} cuisine. Get creative with how the available ingredients could work in a ${randomCuisine}-inspired dish.`;
}
try {
const interaction = await ai.interactions.create({
model: 'gemini-2.0-flash',
input: [
{ type: 'text', text: systemPrompt },
{ type: 'text', text: `Here's what I have available: ${ingredients}. What should I eat for lunch?` },
],
});
const suggestion = interaction.output_text;
res.json({ suggestion });
} catch (error) {
console.error('Error calling Gemini:', error.message);
res.status(500).json({ error: 'Something went wrong getting your lunch suggestion. Try again!' });
}
});
app.listen(port, () => {
console.log(`Lunch Decider is running at http://localhost:${port}`);
});
Secret mission
Add a "Surprise Me" Button
Your lunch decider works great for standard suggestions. But what if you want to be surprised? The Surprise Me button constrains Claude to a random cuisine style, turning your fridge contents into something unexpected. Time to see it in action.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing cloud costs. The Gemini API free tier has no credit card requirement and no expiration. You only use quota when you actively make requests.
Resources you used:
- The lunch-decider project folder on your Desktop.
- A Google AI Studio account with an API key.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with your lunch decider.
- Your lunch-decider folder stays on your Desktop. It only uses disk space.
- Your Google AI Studio account remains active. You only use quota when you start the server and make requests.
- The server only runs when you manually start it with node server.js. It does not run in the background.
Pause - I'll come back to this later
There is nothing to pause. The app only runs when you start the server manually.
- If the server is still running in your terminal, press Ctrl+C to stop it.
- Your project files and Google AI Studio account will be waiting for you when you return.
Delete - I don't want to use this again
Remove the project folder and revoke your API key to clean up completely.
Delete the project folder:
macOS
- Open Terminal and run:
rm -rf ~/Desktop/lunch-decider
Windows
- Open Command Prompt and run:
rmdir /s /q %USERPROFILE%\Desktop\lunch-decider
Linux
- Open Terminal and run:
rm -rf ~/Desktop/lunch-decider
Delete your API key:
- Go to aistudio.google.com and sign in.
- Click Get API Key in the left sidebar.
- Find your key in the list, click the trash icon next to it, and confirm deletion.
What about my Google AI Studio account?
Your Google AI Studio account costs nothing if left unused. It is tied to your Google account. You do not need to delete it separately.
Nice Work!
Nice Work!
You just built a fully working AI-powered lunch decider from scratch. You went from an empty folder to a live web app that takes your ingredients, sends them to Gemini, and delivers a decisive lunch recommendation.
You've learned how to:
- Build a Node.js and Express server that calls the Gemini API using the Google GenAI SDK.
- Write a system prompt that shapes an LLM's personality and response style.
- Connect a frontend to a backend REST API endpoint using fetch and handle loading states, errors, and responses.
- Secret Mission: Added a Surprise Me mode that applies a random cuisine constraint to Gemini's suggestions.
Ready to quiz yourself?