Build a Meal Planner AI Chatbot
Build an AI meal planner chatbot with Node.js, Express, and the OpenAI API.
Introduction
30 Second Summary
Figuring out what to cook every day is a surprisingly draining task. Between dietary goals, allergies, and whatever happens to be in your fridge, meal planning can feel like solving a puzzle with missing pieces.
In this project, you will build an AI-powered meal planner chatbot that suggests meals based on your preferences, remembers your conversation, and responds to follow-up questions like a real assistant.
What You'll Build
A locally running web chatbot powered by Node.js and the OpenAI API that plans meals, tracks conversation context, and keeps your API key secure on the server.
By the end of this project, you'll have:
- A Node.js Express server that securely proxies requests to the OpenAI API without exposing your key to the browser.
- A responsive chat interface built with HTML, CSS, and vanilla JavaScript that displays user and assistant messages in real time.
- A custom system prompt that constrains the AI to behave as a nutrition-aware meal planning assistant with conversation memory.
- Secret Mission: a dietary profile feature where saved preferences like allergies and calorie targets are automatically applied to every response.
Are there any prerequisites?
No prior experience is required. This project walks you through installing every tool you need, including Node.js and VS Code. You will need an OpenAI account with a small amount of credit (around $5) to make API calls.
Set Up Your Development Environment
To build your meal planner chatbot, you need three things: a JavaScript runtime to power your server, a code editor to write your files, and access to an AI model through an API.
In this step, you'll install Node.js and VS Code, then create an OpenAI account with an API key that your chatbot will use to generate meal suggestions.
In this step, get ready to:
- Install Node.js (the JavaScript runtime for your server).
- Install VS Code (your code editor).
- Create an OpenAI account and generate an API key.
Install Node.js
Node.js is the runtime that lets you run JavaScript outside of a browser. Your chatbot's server will run on Node.js.
- 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 --version
Look at the output in your terminal and pick the tab that matches what you see.
✔️ I see version 20 or higher
You're all set. Node.js is ready to go.
- Confirm npm is also available by running:
npm --version
You should see a version number printed. This means both Node.js and npm are working.
ⓧ I see an older version
Your version of Node.js is too old for this project. The OpenAI SDK requires Node.js 20 or later.
- Go to https://nodejs.org in your browser.
- Download the LTS version (this replaces your old version).
- Run the installer and accept all defaults.
- Close your terminal, reopen it, and run node --version again to confirm you see version 20 or higher.
macOS users with an old system Node
If node --version still shows an older version after installing, your system may be resolving to a pre-installed copy. Restarting your terminal usually fixes this. If not, use the full path printed by the installer or use a version manager like nvm.
ⓧ Command not found
Node.js is not installed yet. Let's install it now.
- Go to https://nodejs.org in your browser.
- Download the LTS version for your operating system.
- Run the installer and accept all the default settings.
- Close your terminal and reopen it (this loads the new PATH).
- Run node --version again. You should now see a version number starting with 20 or higher.
- Confirm npm is also available by running:
npm --version
You should see a version number printed. npm is installed automatically alongside Node.js.
Still seeing 'command not found' after installing?
Make sure you fully closed and reopened your terminal after installing. On macOS, quitting Terminal from the dock is not enough. Use Cmd+Q to fully quit, then reopen.
On Windows, if you used the installer, try restarting your computer to ensure PATH changes take effect.
Help me fix my Node.js installation.
Install VS Code
VS Code is the code editor you'll use to write all your project files. It has a built-in terminal, file explorer, and extensions that make coding easier.
- Go to https://code.visualstudio.com in your browser.
- Click the download button for your operating system (macOS, Windows, or Linux).
- Run the installer and follow the prompts to complete the installation.
- Verify VS Code is installed by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and confirm it appears in your results.
Already have VS Code?
If VS Code is already installed, you can skip this substep. Any version works for this project.
Create your OpenAI account and generate an API key
Your chatbot sends messages to OpenAI's API to get AI-generated meal suggestions back. To use the API, you need an account, an API key, and a small amount of prepaid credit.
- Go to https://platform.openai.com in your browser.
- Sign up for a new account (or log in if you already have one).
- Once logged in, click Settings in the left sidebar.
- Select Billing from the menu.
- Click Add payment method and enter your card details.
- Add at least $5 of credit to your account. This is required for API access and will last through many projects at the model's low cost.
Why do I need to add credit?
OpenAI's API uses a pay-as-you-go model. The model you'll use in this project (gpt-4.1-nano) costs $0.10 per million input tokens and $0.40 per million output tokens. A full session of testing your chatbot will cost well under $0.01.
The $5 minimum gives you enough credit for hundreds of test sessions across multiple projects.
- Now navigate to API Keys in the left sidebar (under Settings).
- Click Create new secret key.
- Give your key a name like meal-planner and click Create secret key.
- Copy your API key immediately and save it somewhere safe (like a password manager or a temporary note). You will not be able to see this key again after closing the dialog.
Keep your API key secret
Your API key is like a password. Never share it publicly, paste it in a chat, or commit it to a git repository. In the next step, you'll store it securely in a .env file that stays on your machine.
Can't find API Keys in the sidebar?
You can go directly to platform.openai.com/api-keys in your browser. Make sure you're logged into your OpenAI account.
Help me find where to create an API key on OpenAI.
Your development environment is ready. Next up, you'll create your project folder, install your dependencies, and configure your API key securely.
Initialize the Project and Install Dependencies
Every Node.js project starts with a folder and a package.json file that tracks its dependencies. Think of package.json as a recipe card listing all the ingredients your project needs to run.
In this step, you'll create your project folder, install the packages that power your chatbot, and set up secure storage for your OpenAI API key.
In this step, get ready to:
- Create a project folder and initialize it with npm.
- Install Express, OpenAI, and dotenv packages.
- Create environment files to keep your API key secure.
Create the project folder and initialize it
Your project needs its own folder on your Desktop so you can find it easily.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir meal-planner-ai
cd meal-planner-ai
- Confirm the folder exists by running:
ls ~/Desktop
You should see meal-planner-ai listed in the output.
Now open your project in VS Code.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio 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 meal-planner-ai folder, and click Open.
Next, open VS Code's built-in terminal so you can run commands without leaving the editor.
- Click Terminal in the top menu bar.
- Click New Terminal.
What is the integrated terminal?
VS Code has a built-in terminal panel at the bottom of the window. It works exactly like your system terminal, but opens directly inside your project folder.
This means every command you run is already pointing at the right place.
- Initialize your project by running this command in the integrated terminal:
npm init -y
What does this command do?
The npm init command creates a package.json file that describes your project.
The -y flag accepts all the defaults so you skip the interactive questionnaire.
You should see a JSON output in the terminal showing your new package.json contents.
Not seeing the package.json output?
Make sure you ran the command inside the meal-planner-ai folder. Type pwd (macOS/Linux) or cd (Windows) to check your current location.
If you see a different folder, run cd ~/Desktop/meal-planner-ai and try again.
help me fix my npm init command
Install the required packages
Your chatbot needs three packages to work. Express creates the web server. The OpenAI package connects to the AI model. And dotenv loads your API key from a file.
- Install all three packages by running:
npm install express openai dotenv
What do these packages do?
- express creates a web server that handles HTTP requests and serves your chat interface.
- openai is the official SDK for calling OpenAI's AI models from Node.js.
- dotenv reads your .env file and loads its values into your app as environment variables.
You should see output showing the packages being downloaded. A node_modules/ folder and a package-lock.json file now appear in your project.
- Confirm the packages installed by running:
ls
You should see node_modules, package.json, and package-lock.json in the output.
Seeing permission errors or install failures?
On macOS, if you see an EACCES error, try prefixing the command with sudo. On Windows, try running VS Code as Administrator.
If you see network errors, check your internet connection and try running the install command again.
help me fix my npm install error
Secure your API key with environment files
Your OpenAI API key is a secret that should never appear in your code directly. Environment variables let your app read the key at runtime without exposing it in files you might share or commit to version control.
- In the VS Code Explorer sidebar, click the new file icon (or right-click the empty space) to create a new file.
- Name the file .env and press Enter.
- Add the following line to your .env file, replacing your-api-key-here with the actual API key you generated in Step 1:
OPENAI_API_KEY=your-api-key-here
Why not paste the key directly in code?
If you paste your API key directly into your JavaScript files, anyone who sees your code also sees your key. They could use it to make API calls billed to your account.
A .env file keeps the key separate from your code. The dotenv package loads it into your app at startup without it ever appearing in source files.
Now create a .gitignore file to prevent your secret key and bulky dependencies from being tracked by version control.
- Create another new file in the VS Code Explorer sidebar.
- Name it .gitignore and press Enter.
- Add the following lines to your .gitignore file:
node_modules/
.env
What does .gitignore do?
When you use Git for version control, .gitignore tells Git which files to skip.
The node_modules/ folder contains thousands of dependency files that anyone can regenerate with npm install. The .env file holds your secret key. Neither belongs in a shared repository.
- Save both files by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Check your VS Code Explorer sidebar. You should now see five items: .env, .gitignore, node_modules, package-lock.json, and package.json.
Don't see the .env or .gitignore files?
Files starting with a dot are hidden by default on some systems. In VS Code, they always show in the Explorer sidebar. If you don't see them, make sure you saved after creating them.
Also check that you typed the filename starting with a dot (.env not env).
help me find my .env file
✔️ Awesome, I've got everything!
Great. Double check that you saved both .env and .gitignore before moving on.
ⓧ I'd like to double check the full code
Here is what each file should look like at this point.
OPENAI_API_KEY=your-api-key-here
node_modules/
.env
Your project is initialized and your API key is securely stored. Next, you'll build the Express server that receives chat messages and sends them to OpenAI.
Build the Express Server and API Route
In the last step, you set up your project folder with all the dependencies and configuration files it needs. Now it's time to write the backend server that connects your chatbot to the OpenAI API.
Your browser can't call the OpenAI API directly without exposing your secret API key to anyone who inspects the page. Instead, you'll build an Express server that acts as a secure middleman. It takes messages from the browser, sends them to OpenAI along with a system prompt that makes the AI act as a meal planner, and returns the response.
In this step, get ready to:
- Create server.js and configure Express with middleware.
- Define a system prompt that shapes the AI's meal planning personality.
- Build a POST endpoint that sends messages to OpenAI and returns responses.
Set up the Express server and OpenAI client
The server needs three things to get started: Express to handle HTTP requests, the OpenAI SDK to talk to the AI, and dotenv to load your API key from the .env file.
- Create a new file called server.js in the root of your project folder (the same level as package.json).
- Add the following code to set up the server foundation:
const express = require('express');
const OpenAI = require('openai');
const dotenv = require('dotenv');
// Load environment variables from .env file
dotenv.config();
// Create Express app and configure middleware
const app = express();
app.use(express.json());
app.use(express.static('public'));
// Initialize the OpenAI client with your API key
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
What does this code do?
- require('express'), require('openai'), and require('dotenv') import the three packages you installed in the previous step.
- dotenv.config() reads your .env file and loads OPENAI_API_KEY into process.env so your code can access it securely.
- express.json() lets the server understand JSON data sent from the browser.
- express.static('public') tells Express to serve any files in a public folder (which you'll create in the next step for your chat interface).
- new OpenAI(...) creates the OpenAI client that your server uses to communicate with the AI.
You won't see a change yet. The next chunk defines the system prompt that shapes your AI's behaviour.
Define the system prompt
A system prompt is a set of hidden instructions that tells the AI how to behave. The user never sees it, but it shapes every response the AI generates.
- In server.js, add the following constant below the client declaration:
// Define the AI's personality and behaviour rules
const SYSTEM_PROMPT = `You are a friendly and knowledgeable meal planning assistant. Your role is to help users plan meals based on their dietary preferences, available ingredients, nutritional goals, and time constraints.
When suggesting meals:
- Provide clear meal names and brief descriptions
- Include estimated prep/cook times
- Note approximate calorie counts when relevant
- Consider dietary restrictions the user mentions
- Offer balanced nutritional advice
- Format responses clearly with numbered lists or bullet points when suggesting multiple meals
When asked for shopping lists:
- Group items by category (produce, protein, dairy, pantry staples)
- Include approximate quantities
Be conversational, encouraging, and practical. If the user's request is unclear, ask a clarifying question.`;
What does this prompt do?
This prompt turns a general-purpose AI into a meal planning specialist. It gives the AI a friendly tone, tells it to include practical details like prep times and calorie counts, and instructs it to organize shopping lists by category.
Every message the user sends will be paired with this hidden prompt behind the scenes. The AI follows these rules without the user needing to repeat them.
You won't see a change yet. The next chunk uses this prompt to build the chat endpoint.
Create the chat endpoint and start the server
The endpoint is where the real work happens. It receives the user's conversation from the browser, prepends your system prompt, calls the OpenAI Chat Completions API, and sends back the AI's reply.
- Add the following code below the SYSTEM_PROMPT constant in server.js:
// Handle chat messages from the frontend
app.post('/api/chat', async (req, res) => {
try {
const { messages, profile } = req.body;
// Start with the base system prompt
let systemContent = SYSTEM_PROMPT;
// Append dietary profile if the user has set one
if (profile) {
systemContent += `\n\nUser's dietary profile:\n- Diet type: ${profile.diet || 'No preference'}\n- Allergies: ${profile.allergies || 'None'}\n- Daily calorie target: ${profile.calories || 'Not specified'}\n\nAlways consider this profile when making suggestions, even if the user does not mention these constraints.`;
}
// Call the OpenAI Chat Completions API
const completion = await client.chat.completions.create({
model: 'gpt-4.1-nano',
messages: [
{ role: 'system', content: systemContent },
...messages,
],
temperature: 0.7,
max_tokens: 1000,
});
// Send the AI's response back to the browser
res.json({
reply: completion.choices[0].message.content,
});
} catch (error) {
console.error('OpenAI API error:', error.message);
res.status(500).json({ error: 'Failed to get response from AI' });
}
});
// Start the server
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Meal Planner server running at http://localhost:${PORT}`);
});
What does this endpoint do?
- app.post('/api/chat', ...) creates a POST route that the browser sends messages to.
- const { messages, profile } = req.body extracts the conversation history and optional dietary profile from the request.
- client.chat.completions.create(...) sends the system prompt plus all conversation messages to the gpt-4.1-nano model.
- temperature: 0.7 adds some creativity to the responses. A lower value makes the AI more predictable.
- The catch block handles errors gracefully so the server doesn't crash if something goes wrong with the API call.
- app.listen(PORT, ...) starts the server on port 3000 and prints a confirmation message.
- Save server.js.
- Test that the server starts by running this command in your terminal:
node server.js
You should see Meal Planner server running at http://localhost:3000 printed in your terminal.
Why does the browser show an empty page?
If you open http://localhost:3000 in your browser right now, you'll see a blank page or a "Cannot GET /" message. That's expected. The server is looking for files in a public folder that doesn't exist yet. You'll build the chat interface there in the next step.
Server won't start?
- If you see SyntaxError, check that all backticks and curly braces in your template literals are closed properly.
- If you see Cannot find module 'express', make sure you ran npm install express openai dotenv in the previous step and that your terminal is in the correct project folder.
- If the terminal hangs with no output, press Ctrl+C to stop it and check for typos in the app.listen block.
Still stuck? Help me debug why my Node.js server won't start
- Press Ctrl+C in your terminal to stop the server for now.
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js and then move on to building the chat interface.
ⓧ 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 OpenAI = require('openai');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
app.use(express.json());
app.use(express.static('public'));
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const SYSTEM_PROMPT = `You are a friendly and knowledgeable meal planning assistant. Your role is to help users plan meals based on their dietary preferences, available ingredients, nutritional goals, and time constraints.
When suggesting meals:
- Provide clear meal names and brief descriptions
- Include estimated prep/cook times
- Note approximate calorie counts when relevant
- Consider dietary restrictions the user mentions
- Offer balanced nutritional advice
- Format responses clearly with numbered lists or bullet points when suggesting multiple meals
When asked for shopping lists:
- Group items by category (produce, protein, dairy, pantry staples)
- Include approximate quantities
Be conversational, encouraging, and practical. If the user's request is unclear, ask a clarifying question.`;
app.post('/api/chat', async (req, res) => {
try {
const { messages, profile } = req.body;
let systemContent = SYSTEM_PROMPT;
if (profile) {
systemContent += `\n\nUser's dietary profile:\n- Diet type: ${profile.diet || 'No preference'}\n- Allergies: ${profile.allergies || 'None'}\n- Daily calorie target: ${profile.calories || 'Not specified'}\n\nAlways consider this profile when making suggestions, even if the user does not mention these constraints.`;
}
const completion = await client.chat.completions.create({
model: 'gpt-4.1-nano',
messages: [
{ role: 'system', content: systemContent },
...messages,
],
temperature: 0.7,
max_tokens: 1000,
});
res.json({
reply: completion.choices[0].message.content,
});
} catch (error) {
console.error('OpenAI API error:', error.message);
res.status(500).json({ error: 'Failed to get response from AI' });
}
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Meal Planner server running at http://localhost:${PORT}`);
});
Your backend is live and ready to receive messages. Next up, you'll build the chat interface that sends messages to this server and displays the AI's responses.
Create the Chat Interface
Your backend is ready to receive messages and respond with AI-generated meal suggestions. Now you need the frontend that users actually interact with.
In this step, you will build a complete chat interface using HTML, CSS, and vanilla JavaScript. The interface sends user messages to your /api/chat endpoint and displays the AI responses in a chat bubble layout.
In this step, get ready to:
- Create the HTML structure for the chat page.
- Style the chat interface with CSS.
- Add JavaScript to handle sending messages and rendering responses.
Build the HTML page
Your Express server is already configured to serve static files from a public folder. You need to create that folder and add the HTML file inside it.
- Create the public folder inside your project by running this command in your terminal:
mkdir public
- Confirm the folder exists by running:
ls
You should see public listed alongside server.js, node_modules, and package.json.
- In VS Code, right-click the public folder in the Explorer sidebar.
- Click New File and name it index.html.
- Paste the following code 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>Meal Planner AI</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app-container">
<header>
<h1>Meal Planner AI</h1>
<p>Your personal meal planning assistant</p>
</header>
<div class="chat-container">
<div id="messages" class="messages">
<div class="message assistant">
<p>Hi! I'm your meal planning assistant. Tell me about your dietary preferences, what ingredients you have, or ask me to plan meals for the week. How can I help you today?</p>
</div>
</div>
<div class="input-area">
<input type="text" id="user-input" placeholder="Ask me to plan your meals..." autocomplete="off">
<button id="send-btn">Send</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
What does this code do?
This HTML page sets up the structure of your chat interface. It has three main areas:
- A header with the app title and subtitle.
- A #messages div that holds the chat bubbles, pre-loaded with a welcome message from the assistant.
- An .input-area with a text input and send button at the bottom.
- Save public/index.html.
- Open the file directly in your browser by dragging index.html from your file manager into a browser window.
- You should see unstyled text with the heading "Meal Planner AI", the welcome message, a text input, and a Send button.
See a blank page?
- Make sure you saved the file after pasting the code.
- Confirm the file is named exactly index.html inside the public folder, not at the project root.
Still stuck? Help me figure out why my index.html page is blank in the browser.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meal Planner AI</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app-container">
<header>
<h1>Meal Planner AI</h1>
<p>Your personal meal planning assistant</p>
</header>
<div class="chat-container">
<div id="messages" class="messages">
<div class="message assistant">
<p>Hi! I'm your meal planning assistant. Tell me about your dietary preferences, what ingredients you have, or ask me to plan meals for the week. How can I help you today?</p>
</div>
</div>
<div class="input-area">
<input type="text" id="user-input" placeholder="Ask me to plan your meals..." autocomplete="off">
<button id="send-btn">Send</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Style the chat interface
The HTML gives you structure, but it looks bare without styling. You will add CSS to create a polished chat layout with distinct message bubbles for user and assistant messages.
- In VS Code, right-click the public folder in the Explorer sidebar.
- Click New File and name it style.css.
- Paste the following layout and message styles 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: #f0f2f5;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.app-container {
width: 100%;
max-width: 700px;
height: 90vh;
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
header {
padding: 20px;
background: #2d8a4e;
color: white;
text-align: center;
}
header h1 {
font-size: 1.5rem;
margin-bottom: 4px;
}
header p {
font-size: 0.85rem;
opacity: 0.85;
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 80%;
padding: 12px 16px;
border-radius: 12px;
line-height: 1.5;
white-space: pre-wrap;
}
.message.user {
align-self: flex-end;
background: #2d8a4e;
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant {
align-self: flex-start;
background: #f0f2f5;
color: #1a1a1a;
border-bottom-left-radius: 4px;
}
What does this code do?
- The * reset removes default browser margins and padding so your layout starts from a clean slate.
- The .app-container creates a centered white card with rounded corners and a subtle shadow.
- User messages (.message.user) appear green and right-aligned. Assistant messages (.message.assistant) appear grey and left-aligned.
- Now add the input area and button styles below the code you just pasted:
.input-area {
display: flex;
padding: 16px;
border-top: 1px solid #e0e0e0;
gap: 8px;
}
#user-input {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 24px;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
}
#user-input:focus {
border-color: #2d8a4e;
}
#send-btn {
padding: 12px 24px;
background: #2d8a4e;
color: white;
border: none;
border-radius: 24px;
font-size: 0.95rem;
cursor: pointer;
transition: background 0.2s;
}
#send-btn:hover {
background: #236b3e;
}
#send-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
What does this code do?
- The .input-area uses flexbox to put the text field and button side by side at the bottom of the chat.
- The #user-input gets a pill shape with border-radius: 24px and turns green on focus.
- The #send-btn:disabled style greys out the button while a message is being sent, preventing double-clicks.
- Save public/style.css.
- Go back to your browser tab with index.html open and refresh the page.
- You should see a centered white card with a green header, the welcome message in a grey bubble, and a rounded input field with a green Send button.
Page still looks unstyled?
- Make sure style.css is inside the public folder, not at the project root. The HTML links to it with a relative path.
- Check the filename is exactly style.css (not styles.css or style.CSS).
- Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear cached styles.
Still stuck? Help me debug why my CSS file is not loading for my chat interface.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f2f5;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.app-container {
width: 100%;
max-width: 700px;
height: 90vh;
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
header {
padding: 20px;
background: #2d8a4e;
color: white;
text-align: center;
}
header h1 {
font-size: 1.5rem;
margin-bottom: 4px;
}
header p {
font-size: 0.85rem;
opacity: 0.85;
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 80%;
padding: 12px 16px;
border-radius: 12px;
line-height: 1.5;
white-space: pre-wrap;
}
.message.user {
align-self: flex-end;
background: #2d8a4e;
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant {
align-self: flex-start;
background: #f0f2f5;
color: #1a1a1a;
border-bottom-left-radius: 4px;
}
.input-area {
display: flex;
padding: 16px;
border-top: 1px solid #e0e0e0;
gap: 8px;
}
#user-input {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 24px;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
}
#user-input:focus {
border-color: #2d8a4e;
}
#send-btn {
padding: 12px 24px;
background: #2d8a4e;
color: white;
border: none;
border-radius: 24px;
font-size: 0.95rem;
cursor: pointer;
transition: background 0.2s;
}
#send-btn:hover {
background: #236b3e;
}
#send-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
Add the chat logic with JavaScript
The page looks good, but it cannot send or receive messages yet. You need JavaScript to capture the user's input, send it to your backend, and display the AI's response.
- In VS Code, right-click the public folder in the Explorer sidebar.
- Click New File and name it script.js.
- Paste the following setup and message rendering code into public/script.js:
// Grab references to the DOM elements
const messagesContainer = document.getElementById('messages');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
// Store the full conversation so the AI remembers context
let conversationHistory = [];
// Add a new chat bubble to the messages area
function addMessage(content, role) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', role);
messageDiv.textContent = content;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
What does this code do?
- The three const lines grab references to the message container, input field, and send button so you can interact with them in code.
- The conversationHistory array stores every message sent and received. This gets sent with each request so the AI can remember what was said earlier.
- The addMessage function creates a new div, adds the correct CSS class (user or assistant), and scrolls the chat to the bottom.
- Now add the message sending logic and event listeners below the code you just pasted:
// Send the user's message to the backend and display the reply
async function sendMessage() {
const text = userInput.value.trim();
if (!text) return;
addMessage(text, 'user');
conversationHistory.push({ role: 'user', content: text });
userInput.value = '';
sendBtn.disabled = true;
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: conversationHistory }),
});
const data = await response.json();
if (data.error) {
addMessage('Sorry, something went wrong. Please try again.', 'assistant');
} else {
addMessage(data.reply, 'assistant');
conversationHistory.push({ role: 'assistant', content: data.reply });
}
} catch (error) {
addMessage('Sorry, could not connect to the server.', 'assistant');
}
sendBtn.disabled = false;
userInput.focus();
}
// Listen for clicks and Enter key presses
sendBtn.addEventListener('click', sendMessage);
userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendMessage();
});
What does this code do?
- The sendMessage function reads the input, shows the user's message as a bubble, disables the button to prevent double-sends, then calls your /api/chat endpoint.
- It sends the full conversationHistory array each time. This is how the AI remembers earlier messages in the conversation.
- The two event listeners trigger sendMessage when the user clicks the button or presses Enter.
Why send the full conversation history?
The OpenAI Chat Completions API is stateless. It does not remember previous messages on its own. Your frontend must store every message and send the entire history with each new request. This is what gives the chatbot its ability to follow up on earlier answers.
- Save public/script.js.
- You will not see a change in the browser yet. The JavaScript needs the Express server running to reach /api/chat. You will test everything in the next step.
Seeing a console error about getElementById?
- Make sure the <script src="script.js"></script> tag is at the bottom of index.html, just before </body>. If the script runs before the HTML elements exist, getElementById returns null.
- Double-check that the IDs in your HTML (messages, user-input, send-btn) match the IDs referenced in script.js exactly.
Need more help? Help me debug a getElementById returning null in my chat interface script.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
const messagesContainer = document.getElementById('messages');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
let conversationHistory = [];
function addMessage(content, role) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', role);
messageDiv.textContent = content;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
async function sendMessage() {
const text = userInput.value.trim();
if (!text) return;
addMessage(text, 'user');
conversationHistory.push({ role: 'user', content: text });
userInput.value = '';
sendBtn.disabled = true;
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: conversationHistory }),
});
const data = await response.json();
if (data.error) {
addMessage('Sorry, something went wrong. Please try again.', 'assistant');
} else {
addMessage(data.reply, 'assistant');
conversationHistory.push({ role: 'assistant', content: data.reply });
}
} catch (error) {
addMessage('Sorry, could not connect to the server.', 'assistant');
}
sendBtn.disabled = false;
userInput.focus();
}
sendBtn.addEventListener('click', sendMessage);
userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendMessage();
});
Your entire chat interface is built. In the next step, you will start the server and have a real conversation with your AI meal planner.
Test Your Meal Planner Chatbot
Your server code is ready. Your chat interface is styled and wired up. Now it's time to bring everything together and see your meal planner in action.
In this step, you'll start your Express server, open the chatbot in your browser, and test that it responds to meal planning questions. You'll also verify that conversation memory works by sending follow-up messages.
In this step, get ready to:
- Start the Express server and open the chat UI.
- Test the chatbot with a meal planning request.
- Verify conversation memory with a follow-up message.
Start the server
Your Express server needs to be running for the frontend to connect to the OpenAI API. The server listens on port 3000 and serves your chat interface as static files.
- Make sure you're inside your project folder in the terminal (the folder containing server.js).
- Start the server by running this command:
node server.js
- You should see this message in your terminal:
Meal Planner server running at http://localhost:3000
What does this confirm?
This message means your Express server started successfully. It's now serving your public/ folder as static files and listening for POST requests on /api/chat.
- Open your browser and navigate to http://localhost:3000.
You should see the Meal Planner AI chat interface with a green header and the assistant's welcome message.
Don't see the chat interface?
Make sure your terminal still shows the server running. If you closed it or see an error, check that your .env file exists in the same folder as server.js and contains your OpenAI API key.
Also confirm your public/ folder contains index.html, style.css, and script.js. The Express static middleware serves files from this folder.
Help me debug why my server isn't showing the chat interface
Test with a meal planning request
The real test is whether the AI responds with relevant meal suggestions. Your system prompt instructs the model to provide meal names, prep times, and calorie estimates.
- Click inside the text input at the bottom of the chat.
- Type a meal planning request. For example:
Suggest 3 quick dinners for someone who is vegetarian and likes spicy food
- Click Send or press Enter.
Your message appears in a green bubble on the right. After a moment, the assistant responds with a grey bubble containing numbered meal suggestions.
What's happening behind the scenes?
When you press Send, your frontend pushes the message to the conversationHistory array and POSTs it to /api/chat. The server prepends the system prompt and forwards the full conversation to OpenAI's Chat Completions API.
The model (gpt-4.1-nano) generates a response shaped by both the system prompt and your message. The server extracts the reply text and sends it back to the browser.
Seeing an error message instead of a reply?
If the chat shows "Sorry, something went wrong. Please try again." check your terminal for the error details. Common causes:
- Your OpenAI API key in .env is incorrect or hasn't been replaced with your real key.
- Your OpenAI account doesn't have credit loaded. You need at least $5 of prepaid credit for API access.
- A typo in OPENAI_API_KEY in your .env file. Make sure there are no extra spaces or quotes around the key value.
Help me debug why my OpenAI API call is failing
Test conversation memory
The OpenAI Chat Completions API is stateless. It doesn't remember anything between requests. Your frontend solves this by sending the entire conversation history with every message.
- In the same chat session (don't refresh the page), type a follow-up message that references the previous response. For example:
Give me a shopping list for meal number 2
- Click Send or press Enter.
The assistant should respond with a shopping list specifically for the second meal it suggested earlier. This confirms that conversation memory is working.
Why does this prove memory works?
The phrase "meal number 2" only makes sense if the AI can see your earlier message and its own previous response. Your conversationHistory array stores every message exchanged in the session.
Each time you send a new message, the entire array is included in the API request. The model reads through the full conversation to generate a contextual reply.
Try refreshing the page
If you refresh the browser and ask "What was meal number 2?" the AI won't know. Refreshing clears the conversationHistory array in JavaScript, so the context resets. This is expected behaviour for a session-based chat.
AI not remembering context?
If the assistant responds as though it has no context, make sure you didn't refresh the page between messages. Also check that your sendMessage() function pushes both the user message and the assistant reply to conversationHistory.
Help me figure out why my chatbot doesn't remember conversation context
Secret mission
Add a Dietary Profile Feature
Your chatbot works great, but users have to repeat their dietary preferences every time they start a new conversation. What if the chatbot always knew they were vegan, allergic to soy, and aiming for 1800 calories a day?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine. The only ongoing cost comes from OpenAI API calls you actively make while chatting.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Your project folder (containing server.js, public/, .env, and node_modules/).
- Your OpenAI API key (managed at platform.openai.com).
- The running Node.js server process.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with the chatbot.
- Your project files stay on your computer, ready to use anytime.
- Restart the server anytime by running node server.js from your project folder.
- Your OpenAI API key stays active. You are only charged when you actively send messages to the chatbot.
Pause - I'll come back to this later
Stop the running server to free up your terminal. Your files stay on disk, ready to pick back up later.
- In the terminal where the server is running, press Ctrl+C to stop the server.
- Your project folder and all files remain on your machine.
- Your OpenAI API key remains active but incurs no cost while the server is stopped.
Delete - I don't want to use this again
Remove all project resources and revoke API access so nothing lingers on your machine or account.
- In the terminal where the server is running, press Ctrl+C to stop the server.
Delete your project folder by running the appropriate command for your operating system.
macOS
rm -rf ~/Desktop/meal-planner
Windows
rmdir /s /q %USERPROFILE%\Desktop\meal-planner
- Delete your OpenAI API key. Go to platform.openai.com and navigate to API Keys in the left sidebar.
- Find the key you created for this project and click the trash icon to delete it.
Deleted keys cannot be recovered
Make sure you select the correct key before deleting. If you use this key in other projects, those projects will stop working.
Nice Work!
Nice Work!
You just built a fully functional AI-powered meal planner chatbot from scratch. You connected a web frontend to the OpenAI API through a secure Node.js backend, crafted a system prompt that shapes AI behaviour, and managed multi-turn conversation context.
You've learned how to:
- Build a Node.js Express server that securely proxies requests to the OpenAI API, keeping your API key hidden from the browser.
- Create a responsive chat interface with HTML, CSS, and vanilla JavaScript that maintains conversation history across multiple messages.
- Craft a custom system prompt that constrains an AI model to behave as a nutrition-aware meal planning assistant.
- Added a dietary profile feature that injects persistent user preferences into the system prompt, personalizing every response automatically.
Ready to quiz yourself?