← Back to list

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…

Deepak in WebNex Labs · 2026-06-24 06:38 · 27 claps · 7.2 min read
#react #nodejs #javascript #openai #chatgpt
Open on Medium ↗
Wiki topics: LLM · Large Language Models 🌐 · Web Development

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

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

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 integration
  • client/src/App.jsx — React chat interface with message history
  • client/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