AI Project Guide
Build an AI Chatbot
Build a working chatbot with Node.js, Express, and OpenAI. Includes conversation memory, streaming responses, and a clean HTML frontend. Under 45 minutes.
📋 What You'll Need
- Node.js 18+ — see our Install Node.js guide
- An OpenAI API key — see our OpenAI API Setup guide
- A code editor
- 45 minutes
What you'll build: A chatbot that remembers conversation context. Backend with Express, frontend with plain HTML/JS. Streaming responses like ChatGPT.
Set Up the Project
mkdir ai-chatbot
cd ai-chatbot
npm init -y
npm install express openai dotenv
npm install -D nodemon
Create .env:
OPENAI_API_KEY=sk-your-key-here
Add .env and node_modules to .gitignore.
Build the Backend
Create server.js:
import 'dotenv/config';
import express from 'express';
import OpenAI from 'openai';
const app = express();
app.use(express.json());
app.use(express.static('public'));
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
// Store conversation history per session
const sessions = {};
app.post('/api/chat', async (req, res) => {
const { message, sessionId } = req.body;
if (!message || !sessionId) {
return res.status(400).json({ error: 'Missing message or sessionId' });
}
// Initialize session if new
if (!sessions[sessionId]) {
sessions[sessionId] = [
{
role: 'system',
content: 'You are a helpful coding tutor. Keep answers concise.'
}
];
}
// Add user message
sessions[sessionId].push({ role: 'user', content: message });
try {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: sessions[sessionId],
temperature: 0.7
});
const reply = response.choices[0].message.content;
// Add assistant reply to history
sessions[sessionId].push({ role: 'assistant', content: reply });
res.json({ reply });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to get response' });
}
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Chatbot running at http://localhost:${PORT}`);
});
Add "type": "module" to package.json for ES module syntax.
Build the Frontend
Create public/index.html:
<!DOCTYPE html>
<html>
<head>
<title>AI Chatbot</title>
<style>
body { font-family: system-ui; max-width: 600px; margin: 40px auto; padding: 20px; }
#messages { border: 1px solid #ddd; border-radius: 8px; padding: 16px; min-height: 400px; margin-bottom: 12px; }
.user { text-align: right; margin: 8px 0; }
.user span { background: #3b82f6; color: white; padding: 8px 12px; border-radius: 12px; display: inline-block; }
.bot { text-align: left; margin: 8px 0; }
.bot span { background: #f1f5f9; padding: 8px 12px; border-radius: 12px; display: inline-block; }
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px; }
</style>
</head>
<body>
<h1>AI Chatbot</h1>
<div id="messages"></div>
<input id="input" placeholder="Type a message and press Enter" />
<script>
const sessionId = crypto.randomUUID();
const messages = document.getElementById('messages');
const input = document.getElementById('input');
input.addEventListener('keydown', async (e) => {
if (e.key !== 'Enter') return;
const text = input.value.trim();
if (!text) return;
messages.innerHTML += `<div class="user"><span>${text}</span></div>`;
input.value = '';
const loadingId = 'loading-' + Date.now();
messages.innerHTML += `<div class="bot" id="${loadingId}"><span>...</span></div>`;
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, sessionId })
});
const data = await res.json();
document.getElementById(loadingId).innerHTML =
`<span>${data.reply}</span>`;
});
</script>
</body>
</html>
Run It
npx nodemon server.js
Open http://localhost:3000. Type a message. The bot replies. Ask a follow-up — it remembers the context. 🎉
Add Streaming (ChatGPT Feel)
Replace the /api/chat handler with a streaming version:
app.post('/api/chat', async (req, res) => {
const { message, sessionId } = req.body;
if (!message || !sessionId) return res.status(400).end();
if (!sessions[sessionId]) {
sessions[sessionId] = [
{ role: 'system', content: 'You are a helpful coding tutor.' }
];
}
sessions[sessionId].push({ role: 'user', content: message });
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: sessions[sessionId],
stream: true
});
let full = '';
for await (const chunk of stream) {
const token = chunk.choices[0]?.delta?.content || '';
if (token) {
full += token;
res.write(`data: ${JSON.stringify({ token })}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
sessions[sessionId].push({ role: 'assistant', content: full });
});
Frontend needs an EventSource to read the stream — but the effect is text appearing token-by-token like ChatGPT.
🧠 Key Concepts
- Conversation memory — send the full history every request. That's how the bot "remembers."
- Session ID — a unique ID per user keeps separate conversations separate.
- System prompt — set the bot's personality and constraints.
- Streaming — better UX, faster perceived response time.
- Token cost — long conversations cost more because you resend history every time.
🚫 Common Beginner Mistakes
- Exposing the API key to the frontend. Always call OpenAI from the backend.
- Not limiting history length. Trim to the last 10–20 messages or costs spiral.
- Forgetting to handle errors. OpenAI can fail — show users a friendly message.
- No rate limiting. A malicious user could burn through your credit. Add simple rate limits.
- Logging user messages in production without consent. Privacy matters.
🎯 Practice Challenges
- Persist chat history to PostgreSQL so conversations survive restarts
- Add a "clear chat" button that resets the session
- Add RAG: answer questions using your own documents
- Add a model selector (gpt-4o-mini vs gpt-4o)
- Deploy the whole thing to Vercel or Railway