Tutorials Logic, IN info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Website Development
Practice
Quiz Challenge Interview Questions Certification Practice
Tools
Online Compiler JSON Formatter Regex Tester CSS Unit Converter Color Picker
Compiler Tools
React + OpenAI Project

Build AI Chatbot Using React and OpenAI: End-to-End Complete Notes

Learn how to build a real AI chatbot with React, a secure Node.js backend, and the OpenAI Responses API. This guide covers architecture, setup, chat UI, API routes, streaming, prompt design, security, deployment, and common production fixes.

React Chatbot OpenAI API Node.js Backend Streaming UI Production Notes

Published: May 2026 Updated: May 2026

Introduction

An AI chatbot looks simple from the outside: a user types a message, the app sends it to an AI model, and the answer appears in the chat window. A production-quality chatbot, however, needs a few important pieces: a clean React interface, a backend that protects your OpenAI API key, message history handling, loading states, error handling, rate limiting, streaming responses, and deployment configuration.

This tutorial builds a complete React and OpenAI chatbot from scratch. The frontend uses React with Vite. The backend uses Node.js and Express. The AI integration uses the OpenAI JavaScript SDK and the Responses API, which OpenAI documents as the modern endpoint for text generation and model interactions. The examples use a practical model choice, but the structure lets you change models later without rewriting the whole app.

By the end, you will have a working full-stack chatbot that can answer questions, preserve conversation context, stream responses, and be deployed safely. You will also understand why the API key must stay on the server and how to debug common errors like CORS, missing environment variables, 401, 429, slow responses, and broken streaming.

What You Will Build

The final app has two parts:

  • React frontend: chat screen, message list, input form, send button, typing state, markdown-friendly message layout, and optional streaming display.
  • Node.js backend: Express API route that receives messages from React, calls OpenAI securely, and returns the model response.
LayerTechnologyResponsibility
FrontendReact + ViteDisplay chat UI, collect user input, call backend API
BackendNode.js + ExpressProtect API key, validate requests, call OpenAI
AI APIOpenAI Responses APIGenerate assistant responses from conversation input
Config.envStore API key, model name, allowed origin, port
DeploymentVercel/Netlify + Render/Railway/Fly/Node hostHost frontend and backend with environment variables

The key rule is simple: React should never call OpenAI directly with your secret key. Browser code is public. Anyone can inspect JavaScript bundles and steal exposed keys. The backend acts as a safe API proxy.

Prerequisites

  • Basic React knowledge: components, state, props, forms, hooks.
  • Basic Node.js knowledge: npm, Express routes, environment variables.
  • Node.js 20+ recommended.
  • An OpenAI API key from the OpenAI platform dashboard.
  • A code editor such as VS Code.

The project is intentionally framework-light. You can later move the same backend logic into Next.js API routes, Laravel, CodeIgniter, Django, Spring Boot, or serverless functions.

Project Structure

Create one folder with separate frontend and backend apps. Keeping them separate makes deployment and security easier to understand.

Code Example 1
react-openai-chatbot/
  client/
    src/
      App.jsx
      main.jsx
      styles.css
    index.html
    package.json
  server/
    src/
      index.js
      openai.js
    .env
    .env.example
    package.json
  README.md

The frontend only knows about your backend URL. The backend knows about the OpenAI API key.

Step 1: Create the React Frontend

Use Vite because it is fast, simple, and works well for React projects.

Terminal Command 1
mkdir react-openai-chatbot
cd react-openai-chatbot
npm create vite@latest client -- --template react
cd client
npm install
npm run dev

Open the local URL shown by Vite. You should see the default React starter page. We will replace it with the chatbot interface.

Step 2: Create the Node.js Backend

Now create the server app. This backend receives chat messages from React and calls OpenAI.

Terminal Command 1
cd ..
mkdir server
cd server
npm init -y
npm install express cors dotenv openai
npm install --save-dev nodemon

Update server/package.json so Node uses ES modules and nodemon can run the app during development.

Code Example 2
{
  "name": "react-openai-chatbot-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "nodemon src/index.js",
    "start": "node src/index.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.19.2",
    "openai": "^4.0.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.0"
  }
}

If a newer OpenAI SDK version is available when you build, install the latest package with npm install openai@latest.

Step 3: Add Environment Variables

Create server/.env. Do not commit this file.

Environment Config 1
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_MODEL=gpt-5.4-mini
PORT=5000
CLIENT_ORIGIN=http://localhost:5173

Create server/.env.example for teammates or deployment notes.

Environment Config 2
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5.4-mini
PORT=5000
CLIENT_ORIGIN=http://localhost:5173

Model note: OpenAI's current docs show the Responses API examples with GPT-5 family models, and the model page recommends the flagship model for complex reasoning while smaller variants are better when latency and cost matter. For a normal website chatbot, start with a smaller model such as gpt-5.4-mini, then upgrade if your quality requirements demand it.

Step 4: Create the OpenAI Client

Create server/src/openai.js.

Environment Config 1
import OpenAI from "openai";

export const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export const model = process.env.OPENAI_MODEL || "gpt-5.4-mini";

The official SDK reads cleanly in server-side JavaScript. Keep this file small so your route logic stays easy to test.

Step 5: Build a Basic Chat API

Create server/src/index.js. This first version returns the full answer after OpenAI completes generation.

Code Example 1
import "dotenv/config";
import express from "express";
import cors from "cors";
import { openai, model } from "./openai.js";

const app = express();
const port = process.env.PORT || 5000;

app.use(cors({
  origin: process.env.CLIENT_ORIGIN || "http://localhost:5173",
}));
app.use(express.json({ limit: "1mb" }));

app.get("/health", (req, res) => {
  res.json({ ok: true });
});

app.post("/api/chat", async (req, res) => {
  try {
    const { messages } = req.body;

    if (!Array.isArray(messages) || messages.length === 0) {
      return res.status(400).json({ error: "messages array is required" });
    }

    const safeMessages = messages.slice(-12).map((message) => ({
      role: message.role === "assistant" ? "assistant" : "user",
      content: String(message.content || "").slice(0, 4000),
    }));

    const response = await openai.responses.create({
      model,
      instructions: "You are a helpful AI tutor. Give clear, practical answers. If code is useful, include concise examples.",
      input: safeMessages,
    });

    res.json({
      message: response.output_text,
    });
  } catch (error) {
    console.error("Chat API error:", error);
    res.status(500).json({
      error: "Unable to generate a response right now.",
    });
  }
});

app.listen(port, () => {
  console.log(`Chatbot server running on http://localhost:${port}`);
});

Run the backend:

Terminal Command 2
cd server
npm run dev

Test it with curl or Postman:

Terminal Command 3
curl -X POST http://localhost:5000/api/chat \
  -H "Content-Type: application/json" \
  -d "{\"messages\":[{\"role\":\"user\",\"content\":\"Explain React state in simple words\"}]}"

Step 6: Build the React Chat UI

Replace client/src/App.jsx with a complete chat component.

Environment Config 1
import { useMemo, useRef, useState } from "react";
import "./styles.css";

const API_URL = import.meta.env.VITE_API_URL || "http://localhost:5000";

const starterMessages = [
  {
    role: "assistant",
    content: "Hi! Ask me anything about React, JavaScript, APIs, or your project.",
  },
];

export default function App() {
  const [messages, setMessages] = useState(starterMessages);
  const [input, setInput] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const inputRef = useRef(null);

  const canSend = useMemo(
    () => input.trim().length > 0 && !isLoading,
    [input, isLoading]
  );

  async function sendMessage(event) {
    event.preventDefault();

    const text = input.trim();
    if (!text || isLoading) return;

    const nextMessages = [
      ...messages,
      { role: "user", content: text },
    ];

    setMessages(nextMessages);
    setInput("");
    setIsLoading(true);

    try {
      const response = await fetch(`${API_URL}/api/chat`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ messages: nextMessages }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || "Request failed");
      }

      setMessages([
        ...nextMessages,
        { role: "assistant", content: data.message },
      ]);
    } catch (error) {
      setMessages([
        ...nextMessages,
        {
          role: "assistant",
          content: "Sorry, I could not answer right now. Please try again.",
        },
      ]);
    } finally {
      setIsLoading(false);
      inputRef.current?.focus();
    }
  }

  function clearChat() {
    setMessages(starterMessages);
    setInput("");
    inputRef.current?.focus();
  }

  return (
    <main className="chat-page">
      <section className="chat-shell" aria-label="AI chatbot">
        <header className="chat-header">
          <div>
            <p className="eyebrow">React + OpenAI</p>
            <h1>AI Chatbot</h1>
          </div>
          <button type="button" onClick={clearChat} className="ghost-button">
            Clear
          </button>
        </header>

        <div className="message-list">
          {messages.map((message, index) => (
            <article
              key={`${message.role}-${index}`}
              className={`message ${message.role}`}
            >
              <div className="avatar">
                {message.role === "assistant" ? "AI" : "You"}
              </div>
              <p>{message.content}</p>
            </article>
          ))}

          {isLoading && (
            <article className="message assistant">
              <div className="avatar">AI</div>
              <p className="typing">Thinking...</p>
            </article>
          )}
        </div>

        <form className="chat-form" onSubmit={sendMessage}>
          <label className="sr-only" htmlFor="chat-input">
            Message
          </label>
          <textarea
            id="chat-input"
            ref={inputRef}
            value={input}
            onChange={(event) => setInput(event.target.value)}
            placeholder="Ask a question..."
            rows={2}
            onKeyDown={(event) => {
              if (event.key === "Enter" && !event.shiftKey) {
                event.preventDefault();
                sendMessage(event);
              }
            }}
          />
          <button type="submit" disabled={!canSend}>
            {isLoading ? "Sending" : "Send"}
          </button>
        </form>
      </section>
    </main>
  );
}

Create client/.env so React knows where your backend runs:

Environment Config 2
VITE_API_URL=http://localhost:5000

Step 7: Add Chatbot Styling

Replace client/src/styles.css with a clean responsive chat layout.

Code Example 1
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #eef2f7;
  color: #111827;
}

.chat-page {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
}

.chat-shell {
  width: min(920px, 100%);
  height: min(760px, calc(100vh - 48px));
  display: grid;
  grid-template-rows: auto 1fr auto;
  background: #ffffff;
  border: 1px solid #dbe3ef;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 24px 70px rgba(15, 23, 42, 0.14);
}

.chat-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 20px 22px;
  border-bottom: 1px solid #e5e7eb;
  background: #0f172a;
  color: #ffffff;
}

.eyebrow {
  margin: 0 0 4px;
  color: #38bdf8;
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.chat-header h1 {
  margin: 0;
  font-size: 1.35rem;
}

.ghost-button {
  border: 1px solid rgba(255, 255, 255, 0.24);
  background: transparent;
  color: #ffffff;
  border-radius: 10px;
  padding: 9px 14px;
  cursor: pointer;
}

.message-list {
  overflow-y: auto;
  padding: 22px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.message {
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr);
  gap: 12px;
  align-items: start;
}

.message.user {
  grid-template-columns: minmax(0, 1fr) 42px;
}

.message.user .avatar {
  grid-column: 2;
  background: #2563eb;
}

.message.user p {
  grid-column: 1;
  grid-row: 1;
  justify-self: end;
  background: #2563eb;
  color: #ffffff;
}

.avatar {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: #10b981;
  color: #ffffff;
  font-size: 0.76rem;
  font-weight: 800;
}

.message p {
  max-width: 680px;
  margin: 0;
  padding: 13px 15px;
  border-radius: 14px;
  background: #f3f6fb;
  color: #111827;
  line-height: 1.6;
  white-space: pre-wrap;
}

.typing {
  color: #64748b;
}

.chat-form {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 12px;
  padding: 16px;
  border-top: 1px solid #e5e7eb;
  background: #f8fafc;
}

.chat-form textarea {
  resize: none;
  border: 1px solid #cbd5e1;
  border-radius: 12px;
  padding: 12px 14px;
  font: inherit;
  outline: none;
}

.chat-form textarea:focus {
  border-color: #38bdf8;
  box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.18);
}

.chat-form button {
  border: 0;
  border-radius: 12px;
  padding: 0 20px;
  background: #0ea5e9;
  color: #ffffff;
  font-weight: 800;
  cursor: pointer;
}

.chat-form button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 640px) {
  .chat-page {
    padding: 0;
  }

  .chat-shell {
    height: 100vh;
    border-radius: 0;
  }

  .chat-form {
    grid-template-columns: 1fr;
  }

  .chat-form button {
    min-height: 44px;
  }
}

Step 8: Understand Message History and Context

A chatbot needs context to answer follow-up questions. In the basic example, React sends the recent message history with every request. The backend trims it to the last 12 messages to control cost and latency.

This is enough for a beginner project. For a larger app, you have three common options:

  • Client-side short history: easiest, but the chat resets on refresh unless stored.
  • Database-backed sessions: store messages per user and conversation ID.
  • Retrieval-augmented context: use embeddings or file search to add relevant docs, FAQs, or knowledge base snippets.

Do not send unlimited history. Long prompts increase cost, slow down responses, and may include stale instructions. Summarize older conversation turns or store important facts separately.

Step 9: Add Streaming Responses

Streaming makes the chatbot feel faster because users see the answer appear token by token. The exact SDK helpers can evolve, but the backend idea stays the same: request a stream from OpenAI, forward chunks to the browser, and update the assistant message as chunks arrive.

Add a new streaming endpoint in server/src/index.js:

Code Example 1
app.post("/api/chat/stream", async (req, res) => {
  try {
    const { messages } = req.body;

    if (!Array.isArray(messages) || messages.length === 0) {
      return res.status(400).json({ error: "messages array is required" });
    }

    const safeMessages = messages.slice(-12).map((message) => ({
      role: message.role === "assistant" ? "assistant" : "user",
      content: String(message.content || "").slice(0, 4000),
    }));

    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Connection", "keep-alive");

    const stream = await openai.responses.create({
      model,
      instructions: "You are a helpful AI tutor. Be clear, practical, and concise.",
      input: safeMessages,
      stream: true,
    });

    for await (const event of stream) {
      if (event.type === "response.output_text.delta") {
        res.write(event.delta);
      }
    }

    res.end();
  } catch (error) {
    console.error("Streaming chat API error:", error);
    if (!res.headersSent) {
      return res.status(500).json({ error: "Unable to stream response." });
    }
    res.end();
  }
});

Then replace the frontend request logic with a streaming version:

Code Example 2
async function sendMessage(event) {
  event.preventDefault();

  const text = input.trim();
  if (!text || isLoading) return;

  const nextMessages = [...messages, { role: "user", content: text }];
  const assistantIndex = nextMessages.length;

  setMessages([...nextMessages, { role: "assistant", content: "" }]);
  setInput("");
  setIsLoading(true);

  try {
    const response = await fetch(`${API_URL}/api/chat/stream`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages: nextMessages }),
    });

    if (!response.ok || !response.body) {
      throw new Error("Streaming request failed");
    }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let fullText = "";

    while (true) {
      const { value, done } = await reader.read();
      if (done) break;

      fullText += decoder.decode(value, { stream: true });

      setMessages((current) =>
        current.map((message, index) =>
          index === assistantIndex
            ? { ...message, content: fullText }
            : message
        )
      );
    }
  } catch (error) {
    setMessages([
      ...nextMessages,
      { role: "assistant", content: "Sorry, streaming failed. Please try again." },
    ]);
  } finally {
    setIsLoading(false);
  }
}

Streaming is optional, but it is one of the biggest perceived-quality upgrades for chat interfaces.

Step 10: Design the Chatbot Prompt

The instructions field controls how your assistant behaves. Keep it clear and specific. Avoid stuffing too many rules into one prompt.

Code Example 1
const instructions = `
You are a helpful AI tutor for Tutorials Logic.
Audience: beginner and intermediate developers.
Style:
- Explain concepts simply.
- Use examples when useful.
- Keep answers practical.
- If the user asks for code, include runnable code.
- If you are unsure, say what information is missing.
Safety:
- Do not ask for secrets, passwords, API keys, or private data.
- Do not invent APIs. Suggest checking official docs for version-specific details.
`;

For a customer support bot, the instructions might include refund policies and escalation rules. For a coding tutor, include teaching style and preferred stack. For an internal company bot, include boundaries around private data and source citations.

Step 11: Security Checklist

  • Never expose the OpenAI API key in React. Keep it in backend environment variables.
  • Validate request shape. Ensure messages is an array and content is a string.
  • Limit message length. Prevent huge payloads with express.json({ limit: "1mb" }) and string slicing.
  • Restrict CORS. Use your actual frontend domain in production.
  • Add rate limiting. Protect your API from abuse and unexpected costs.
  • Log carefully. Avoid storing sensitive prompts, API keys, tokens, or private user data.
  • Use authentication for private bots. Public demo bots can be abused if there is no rate limit or quota.
  • Handle model errors gracefully. Do not show raw stack traces to users.

A simple Express rate limiter can stop accidental spam:

Terminal Command 1
npm install express-rate-limit
Code Example 2
import rateLimit from "express-rate-limit";

const chatLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 20,
  standardHeaders: true,
  legacyHeaders: false,
});

app.use("/api/chat", chatLimiter);

Step 12: Deployment Notes

Frontend deployment

Build the React app:

Terminal Command 1
cd client
npm run build

Deploy client/dist to Vercel, Netlify, Cloudflare Pages, Hostinger static hosting, or any static host. Set:

Environment Config 2
VITE_API_URL=https://your-backend-domain.com

Backend deployment

Deploy the Express backend to Render, Railway, Fly.io, a VPS, Docker, or any Node-compatible host. Set these production environment variables:

Environment Config 3
OPENAI_API_KEY=sk-your-production-key
OPENAI_MODEL=gpt-5.4-mini
PORT=5000
CLIENT_ORIGIN=https://your-frontend-domain.com

Production checklist

  • Use HTTPS for frontend and backend.
  • Set CORS to the real frontend domain, not *.
  • Enable request logging and uptime monitoring.
  • Add rate limits and usage quotas.
  • Keep API keys in host environment settings, never in Git.
  • Test the deployed frontend with the deployed backend URL.

Troubleshooting Common Errors

ProblemLikely CauseFix
401 UnauthorizedMissing or wrong API keyCheck OPENAI_API_KEY, restart backend, verify key in platform dashboard.
429 Rate limitToo many requests or quota issueAdd rate limiting, reduce retries, check project limits and billing.
CORS errorFrontend origin not allowedSet CLIENT_ORIGIN to the exact frontend URL.
React shows empty answerBackend response shape mismatchConfirm backend returns { "message": "..." }.
Streaming never appearsProxy buffers response or wrong event handlingTest locally first, disable buffering on host, verify stream event names.
Slow responsesLarge prompt/history or expensive modelTrim history, summarize older messages, use a smaller model.
API key exposed in browserOpenAI call placed in ReactMove OpenAI call to backend immediately and rotate the exposed key.

Next Improvements

  • Markdown rendering: use a safe markdown renderer for code blocks and lists.
  • Conversation IDs: store chats in a database by user and conversation.
  • Authentication: require login before using paid AI features.
  • File uploads: let users ask questions about uploaded PDFs or docs.
  • Knowledge base: add retrieval so the bot can answer from your tutorials or company docs.
  • Feedback buttons: collect thumbs up/down to improve prompts and detect bad answers.
  • Admin analytics: track total chats, common questions, errors, latency, and estimated cost.
  • Safety layer: add moderation or custom rules for public-facing bots.

Official References

Use official docs when upgrading models, SDK versions, or API parameters:

FAQs

1. Can I call OpenAI directly from React?
Answer: No. React code runs in the browser, so your API key would be visible. Use a backend route.
2. Which OpenAI API should I use for a chatbot?
Answer: Use the Responses API for modern text generation and multi-turn style interactions.
3. Which model should I start with?
Answer: For a normal website chatbot, start with a smaller, lower-latency model such as gpt-5.4-mini, then test quality and upgrade if needed.
4. Do I need a database?
Answer: Not for a demo. Use a database when you want user accounts, saved conversations, analytics, or long-term memory.
5. Why is streaming useful?
Answer: Streaming improves perceived speed because users see the answer as it is generated.
6. How do I reduce cost?
Answer: Trim history, use concise instructions, choose a smaller model, cache repeated answers, and add rate limits.
7. How do I make the chatbot answer from my website content?
Answer: Add retrieval: store your content in a searchable index, fetch relevant snippets, and include them in the model input.
8. Can I deploy frontend and backend separately?
Answer: Yes. Host React as a static app and Express as a Node service. Configure VITE_API_URL and CORS correctly.
9. What if users abuse the chatbot?
Answer: Add authentication, rate limiting, quotas, logging, and abuse monitoring.
10. Should I store every prompt?
Answer: Only if your privacy policy allows it. Avoid storing secrets, sensitive user data, or unnecessary private content.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.