Build a Simple AI Chatbot Using React and Node.js
ChatGPT made AI chatbots feel magical — but the underlying mechanics are simpler than you think. In this guide, we’ll build a fully…
Build a Simple AI Chatbot Using React and Node.js
ChatGPT made AI chatbots feel magical — but the underlying mechanics are simpler than you think. In this guide, we’ll build a fully working chatbot from scratch using React on the frontend and Node.js + Express on the backend, connected to the OpenAI API.
By the end, you’ll have a real chat interface where you can have a back-and-forth conversation with an AI, just like ChatGPT.
Photo by Mohamed Nohassi on Unsplash
What We’re Building
- A React frontend with a clean chat UI (message bubbles, input box, send button)
- A Node.js + Express backend that talks to the OpenAI API
- Conversation memory so the AI remembers what was said earlier
- Streaming responses so replies appear word by word (like ChatGPT)
Tech Stack
Layer Technology Frontend React + Vite Backend Node.js + Express AI OpenAI API (gpt-4o-mini) Styling Plain CSS Communication REST API + Fetch
Prerequisites
- Node.js 18+
- An OpenAI API key — get one at platform.openai.com
Project Structure
chatbot/
├── client/ # React frontend
│ ├── src/
│ │ ├── App.jsx
│ │ ├── App.css
│ │ └── main.jsx
│ └── package.json
│
├── server/ # Node.js backend
│ ├── index.js
│ └── package.json
│
└── README.md
Step 1: Set Up the Backend
mkdir chatbot && cd chatbot
mkdir server && cd server
npm init -y
npm install express cors dotenv openai
server/.env
OPENAI_API_KEY=your_openai_api_key_here
PORT=5000
server/index.js
const express = require('express');
const cors = require('cors');
const OpenAI = require('openai');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// System prompt — defines your bot's personality
const SYSTEM_PROMPT = {
role: 'system',
content: `You are a helpful, friendly assistant.
Keep your answers clear and concise.
If you don't know something, say so honestly.`,
};
// POST /api/chat
// Body: { messages: [ { role, content }, ... ] }
app.post('/api/chat', async (req, res) => {
const { messages } = req.body;
if (!messages || !Array.isArray(messages)) {
return res.status(400).json({ error: 'messages array is required' });
}
try {
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [SYSTEM_PROMPT, ...messages],
max_tokens: 500,
temperature: 0.7,
});
const reply = completion.choices[0].message;
res.json({ message: reply });
} catch (error) {
console.error('OpenAI error:', error.message);
res.status(500).json({ error: 'Failed to get response from AI' });
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Start the server:
node index.js
# Server running on http://localhost:5000
Test it quickly with curl:
curl -X POST http://localhost:5000/api/chat \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Hello!"}]}'
Step 2: Set Up the React Frontend
cd .. # back to chatbot/
npm create vite@latest client -- --template react
cd client
npm install
Step 3: Build the Chat UI
Replace src/App.jsx with the following:
src/App.jsx
import { useState, useRef, useEffect } from 'react';
import './App.css';
const API_URL = 'http://localhost:5000/api/chat';
function App() {
const [messages, setMessages] = useState([
{ role: 'assistant', content: 'Hi! I\'m your AI assistant. How can I help you today?' }
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const bottomRef = useRef(null);
// Auto-scroll to latest message
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = async () => {
const trimmed = input.trim();
if (!trimmed || isLoading) return;
// Add user message to chat
const userMessage = { role: 'user', content: trimmed };
const updatedMessages = [...messages, userMessage];
setMessages(updatedMessages);
setInput('');
setIsLoading(true);
try {
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// Send full conversation history so AI has memory
body: JSON.stringify({ messages: updatedMessages }),
});
if (!res.ok) throw new Error('Server error');
const data = await res.json();
setMessages(prev => [...prev, data.message]);
} catch (err) {
setMessages(prev => [
...prev,
{ role: 'assistant', content: 'Sorry, something went wrong. Please try again.' }
]);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e) => {
// Send on Enter, new line on Shift+Enter
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
const clearChat = () => {
setMessages([
{ role: 'assistant', content: 'Chat cleared! How can I help you?' }
]);
};
return (
<div className="chat-app">
{/* Header */}
<div className="chat-header">
<div className="bot-info">
<div className="bot-avatar">🤖</div>
<div>
<div className="bot-name">AI Assistant</div>
<div className="bot-status">
{isLoading ? 'Typing...' : 'Online'}
</div>
</div>
</div>
<button className="clear-btn" onClick={clearChat}>Clear Chat</button>
</div>
{/* Messages */}
<div className="chat-messages">
{messages.map((msg, i) => (
<div key={i} className={`message-row ${msg.role}`}>
{msg.role === 'assistant' && (
<div className="avatar">🤖</div>
)}
<div className={`bubble ${msg.role}`}>
{msg.content}
</div>
{msg.role === 'user' && (
<div className="avatar">👤</div>
)}
</div>
))}
{/* Typing indicator */}
{isLoading && (
<div className="message-row assistant">
<div className="avatar">🤖</div>
<div className="bubble assistant typing">
<span></span><span></span><span></span>
</div>
</div>
)}
<div ref={bottomRef} />
</div>
{/* Input */}
<div className="chat-input-area">
<textarea
className="chat-input"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a message... (Enter to send)"
rows={1}
disabled={isLoading}
/>
<button
className="send-btn"
onClick={sendMessage}
disabled={!input.trim() || isLoading}
>
Send ➤
</button>
</div>
</div>
);
}
export default App;
Step 4: Style the Chat
Replace src/App.css with:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #f0f2f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.chat-app {
width: 420px;
height: 680px;
background: white;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
.chat-header {
background: #6c63ff;
color: white;
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.bot-info {
display: flex;
align-items: center;
gap: 12px;
}
.bot-avatar {
font-size: 28px;
background: rgba(255,255,255,0.2);
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.bot-name {
font-weight: 700;
font-size: 16px;
}
.bot-status {
font-size: 12px;
opacity: 0.85;
}
.clear-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
}
.clear-btn:hover {
background: rgba(255,255,255,0.3);
}
/* Messages area */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message-row {
display: flex;
align-items: flex-end;
gap: 8px;
}
.message-row.user {
flex-direction: row-reverse;
}
.avatar {
font-size: 22px;
flex-shrink: 0;
}
/* Chat bubbles */
.bubble {
max-width: 75%;
padding: 12px 16px;
border-radius: 18px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
}
.bubble.user {
background: #6c63ff;
color: white;
border-bottom-right-radius: 4px;
}
.bubble.assistant {
background: #f1f0ff;
color: #333;
border-bottom-left-radius: 4px;
}
/* Typing animation */
.bubble.typing {
display: flex;
gap: 5px;
align-items: center;
padding: 14px 18px;
}
.bubble.typing span {
width: 8px;
height: 8px;
background: #6c63ff;
border-radius: 50%;
animation: bounce 1.2s infinite;
}
.bubble.typing span:nth-child(2) { animation-delay: 0.2s; }
.bubble.typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-6px); }
}
/* Input area */
.chat-input-area {
padding: 16px;
border-top: 1px solid #eee;
display: flex;
gap: 10px;
align-items: flex-end;
}
.chat-input {
flex: 1;
border: 1.5px solid #e0e0e0;
border-radius: 12px;
padding: 10px 14px;
font-size: 14px;
resize: none;
outline: none;
font-family: inherit;
line-height: 1.5;
max-height: 100px;
overflow-y: auto;
transition: border-color 0.2s;
}
.chat-input:focus {
border-color: #6c63ff;
}
.send-btn {
background: #6c63ff;
color: white;
border: none;
padding: 10px 18px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.2s, transform 0.1s;
}
.send-btn:hover:not(:disabled) {
background: #5a52e0;
transform: scale(1.03);
}
.send-btn:disabled {
background: #c4c0f5;
cursor: not-allowed;
}
Step 5: Run the App
Open two terminals:
# Terminal 1 — Backend
cd server && node index.js
# Terminal 2 — Frontend
cd client && npm run dev
Visit http://localhost:5173 and your chatbot is live! 🎉
How Conversation Memory Works
The key to making the AI remember previous messages is sending the full conversation history with every request. That’s exactly what we do:
// We always send the entire messages array
body: JSON.stringify({ messages: updatedMessages })
And on the backend, we prepend the system prompt and pass everything to OpenAI:
messages: [SYSTEM_PROMPT, ...messages]
OpenAI’s API is stateless — it has no memory between calls. So we pass the memory ourselves.
Bonus: Add Streaming Responses
Want the AI to type out responses word-by-word like ChatGPT? Replace the backend route with this:
app.post('/api/chat/stream', async (req, res) => {
const { messages } = req.body;
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: [SYSTEM_PROMPT, ...messages],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content || '';
if (delta) {
res.write(`data: ${JSON.stringify({ delta })}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
});
Then in React, read the stream with EventSource or ReadableStream and append characters as they arrive.
Bonus: Build Without OpenAI (Free Option)
Don’t have an OpenAI key? Use a simple rule-based bot instead:
// server/index.js — replace the OpenAI call with this
function getBotReply(userMessage) {
const msg = userMessage.toLowerCase();
if (msg.includes('hello') || msg.includes('hi'))
return 'Hello! How can I help you today?';
if (msg.includes('your name'))
return 'I\'m SimpleBot, your friendly assistant!';
if (msg.includes('help'))
return 'I can answer questions and have a conversation with you.';
if (msg.includes('bye'))
return 'Goodbye! Have a great day 👋';
return 'Interesting! Tell me more about that.';
}
app.post('/api/chat', (req, res) => {
const { messages } = req.body;
const lastMessage = messages[messages.length - 1].content;
const reply = getBotReply(lastMessage);
res.json({ message: { role: 'assistant', content: reply } });
});
What to Build Next
Once you have the basics working, here are great ways to level it up:
- Persist chat history — save messages to a database (MongoDB or SQLite) so chats survive page refreshes
- Multiple chat sessions — let users create and switch between different conversations
- Custom bot personalities — change the system prompt to create a customer support bot, a coding assistant, or a recipe bot
- User authentication — add login so each user has their own chat history
- Voice input — use the Web Speech API to let users speak instead of type
- Deploy to production — host the backend on Railway or Render, the frontend on Vercel
Final Thoughts
Building a chatbot used to require NLP expertise and weeks of training data. Today, with the OpenAI API, you get a state-of-the-art AI brain in 30 lines of Node.js code.
The real skill is in the product layer — designing the UI, managing conversation state, and crafting a system prompt that makes the bot genuinely useful. That’s exactly what we built today.
Full source code overview:
server/index.js— Express API with OpenAI integrationclient/src/App.jsx— React chat interface with message historyclient/src/App.css— Clean, responsive chat styling
If this was helpful, follow for more guides on building real apps with modern web tech.
메타데이터
- post_id
- 6e40be618a17
- slug
- build-a-simple-ai-chatbot-using-react-and-node-js-6e40be618a17
- url
- https://medium.com/webnex-labs/build-a-simple-ai-chatbot-using-react-and-node-js-6e40be618a17
- canonical_url
- https://medium.com/webnex-labs/build-a-simple-ai-chatbot-using-react-and-node-js-6e40be618a17
- author_url
- https://medium.com/@deepakjais
- status
- ok
- fetched_at
- 2026-06-24 23:31:39