How to Build a Chatbot with OpenAI’s API and Deploy It Worldwide (Step-by-Step Tutorial)

Step-by-step guide to build a fully functional OpenAI chatbot with pre-designed UI, dark/light themes, file uploads, and global deployment options.

Build a Fully Functional OpenAI Chatbot (Dark/Light Theme) — Step-by-Step Guide

Learn how to create a production-ready OpenAI chatbot with pre-designed UI (dark & light theme), file uploads, theme toggle, typing effects, and suggestions. Users only need to add their API key. This guide covers frontend, backend, deployment, and best practices.

openai-chatbot

Info!
This tutorial assumes basic knowledge of HTML, CSS, and JavaScript. We'll use OpenAI API securely via a backend proxy and create a fully interactive chatbot UI.

Step 1: Project Setup

Create a folder and initialize Node.js project:



mkdir openai-chatbot && cd openai-chatbot
npm init -y
npm install express dotenv node-fetch

Tip: We'll also use plain HTML/CSS/JS for the frontend. Later you can replace it with React or Next.js if desired.

Step 2: Frontend — Chatbot HTML Structure

Use the following HTML structure:


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>OpenAI Chatbot</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <header class="app-header">
      <h1 class="heading">Hello, there</h1>
      <h4 class="sub-heading">How can I help you today?</h4>
    </header>

    <ul class="suggestions">
      <li class="suggestions-item"><p class="text">Design a home office setup for under $500.</p><span class="icon material-symbols-rounded">draw</span></li>
      <li class="suggestions-item"><p class="text">Improve web development skills in 2025.</p><span class="icon material-symbols-rounded">lightbulb</span></li>
      <li class="suggestions-item"><p class="text">Debug JavaScript faster.</p><span class="icon material-symbols-rounded">explore</span></li>
      <li class="suggestions-item"><p class="text">Create a React todo list component.</p><span class="icon material-symbols-rounded">code_blocks</span></li>
    </ul>

    <div class="chats-container"></div>

    <div class="prompt-container">
      <div class="prompt-wrapper">
        <form class="prompt-form">
          <input type="text" class="prompt-input" placeholder="Ask Anything" required />
          <div class="prompt-actions">
            <div class="file-upload-wrapper">
              <img src="#" class="file-preview" />
              <input type="file" id="file-input" accept="image/*,.pdf,.txt,.csv" hidden />
              <button type="button" class="file-icon material-symbols-rounded">description</button>
              <button type="button" id="cancel-file-btn" class="material-symbols-rounded">close</button>
              <button type="button" id="add-file-btn" class="material-symbols-rounded">attach_file</button>
            </div>
            <button type="button" id="stop-response-btn" class="material-symbols-rounded">stop_circle</button>
            <button type="submit" id="send-prompt-btn" class="material-symbols-rounded">arrow_upward</button>
          </div>
        </form>
        <button id="theme-toggle-btn" class="material-symbols-rounded">light_mode</button>
        <button id="delete-chats-btn" class="material-symbols-rounded">delete</button>
      </div>
      <p class="disclaimer-text">OpenAI can make mistakes, so double-check it.</p>
    </div>
  </div>
  <script src="script.js"></script>
</body>
</html>

Step 3: Frontend Styling (CSS)

Include dark/light theme, chat container, prompt input, suggestions, avatars, and responsive design:


/* Import Google Font - Poppins */
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}
:root {
  /* Dark theme colors */
  --text-color: #edf3ff;
  --subheading-color: #97a7ca;
  --placeholder-color: #c3cdde;
  --primary-color: #101623;
  --secondary-color: #283045;
  --secondary-hover-color: #333e58;
  --scrollbar-color: #626a7f;
}
body.light-theme {
  /* Light theme colors */
  --text-color: #090c13;
  --subheading-color: #7b8cae;
  --placeholder-color: #606982;
  --primary-color: #f3f7ff;
  --secondary-color: #dce6f9;
  --secondary-hover-color: #d2ddf2;
  --scrollbar-color: #a2aac2;
}
body {
  color: var(--text-color);
  background: var(--primary-color);
}
.container {
  overflow-y: auto;
  padding: 32px 0 60px;
  height: calc(100vh - 127px);
  scrollbar-color: var(--scrollbar-color) transparent;
}
.container :where(.app-header, .suggestions, .message, .prompt-wrapper) {
  position: relative;
  margin: 0 auto;
  width: 100%;
  padding: 0 20px;
  max-width: 990px;
}
.container .app-header {
  margin-top: 3vh;
}
.app-header .heading {
  width: fit-content;
  font-size: 3rem;
  background: linear-gradient(to right, #1d7efd, #8f6fff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
.app-header .sub-heading {
  font-size: 2.6rem;
  margin-top: -5px;
  color: var(--subheading-color);
}
.container .suggestions {
  width: 100%;
  list-style: none;
  display: flex;
  gap: 15px;
  margin-top: 9.5vh;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
body.chats-active .container :where(.app-header, .suggestions) {
  display: none;
}
.suggestions .suggestions-item {
  cursor: pointer;
  padding: 18px;
  width: 228px;
  flex-shrink: 0;
  display: flex;
  scroll-snap-align: center;
  flex-direction: column;
  align-items: flex-end;
  border-radius: 12px;
  justify-content: space-between;
  background: var(--secondary-color);
  transition: 0.3s ease;
}
.suggestions .suggestions-item:hover {
  background: var(--secondary-hover-color);
}
.suggestions .suggestions-item .text {
  font-size: 1.1rem;
}
.suggestions .suggestions-item .icon {
  width: 45px;
  height: 45px;
  display: flex;
  font-size: 1.4rem;
  margin-top: 35px;
  align-self: flex-end;
  align-items: center;
  border-radius: 50%;
  justify-content: center;
  color: #1d7efd;
  background: var(--primary-color);
}
.suggestions .suggestions-item:nth-child(2) .icon {
  color: #28a745;
}
.suggestions .suggestions-item:nth-child(3) .icon {
  color: #ffc107;
}
.suggestions .suggestions-item:nth-child(4) .icon {
  color: #6f42c1;
}
.container .chats-container {
  display: flex;
  gap: 20px;
  flex-direction: column;
}
.chats-container .message {
  display: flex;
  gap: 11px;
  align-items: center;
}
.chats-container .message .avatar {
  width: 43px;
  height: 43px;
  flex-shrink: 0;
  align-self: flex-start;
  border-radius: 50%;
  padding: 6px;
  margin-right: -7px;
  background: var(--secondary-color);
  border: 1px solid var(--secondary-hover-color);
}
.chats-container .message.loading .avatar {
  animation: rotate 3s linear infinite;
}
@keyframes rotate {
  100% {
    transform: rotate(360deg);
  }
}
.chats-container .message .message-text {
  padding: 3px 16px;
  word-wrap: break-word;
  white-space: pre-line;
}
.chats-container .bot-message {
  margin: 9px auto;
}
.chats-container .user-message {
  flex-direction: column;
  align-items: flex-end;
}
.chats-container .user-message .message-text {
  padding: 12px 16px;
  max-width: 75%;
  background: var(--secondary-color);
  border-radius: 13px 13px 3px 13px;
}
.chats-container .user-message .img-attachment {
  margin-top: -7px;
  width: 50%;
  border-radius: 13px 3px 13px 13px;
}
.chats-container .user-message .file-attachment {
  display: flex;
  gap: 6px;
  align-items: center;
  padding: 10px;
  margin-top: -7px;
  border-radius: 13px 3px 13px 13px;
  background: var(--secondary-color);
}
.chats-container .user-message .file-attachment span {
  color: #1d7efd;
}
.container .prompt-container {
  position: fixed;
  width: 100%;
  left: 0;
  bottom: 0;
  padding: 16px 0;
  background: var(--primary-color);
}
.prompt-container :where(.prompt-wrapper, .prompt-form, .prompt-actions) {
  display: flex;
  gap: 12px;
  height: 56px;
  align-items: center;
}
.prompt-container .prompt-form {
  height: 100%;
  width: 100%;
  border-radius: 130px;
  background: var(--secondary-color);
}
.prompt-form .prompt-input {
  width: 100%;
  height: 100%;
  background: none;
  outline: none;
  border: none;
  font-size: 1rem;
  color: var(--text-color);
  padding-left: 24px;
}
.prompt-form .prompt-input::placeholder {
  color: var(--placeholder-color);
}
.prompt-wrapper button {
  width: 56px;
  height: 100%;
  flex-shrink: 0;
  cursor: pointer;
  border-radius: 50%;
  font-size: 1.4rem;
  border: none;
  color: var(--text-color);
  background: var(--secondary-color);
  transition: 0.3s ease;
}
.prompt-wrapper :is(button:hover, #cancel-file-btn, .file-icon) {
  background: var(--secondary-hover-color);
}
.prompt-form .prompt-actions {
  gap: 5px;
  margin-right: 7px;
}
.prompt-wrapper .prompt-form :where(.file-upload-wrapper, button, img) {
  position: relative;
  height: 45px;
  width: 45px;
}
.prompt-form .prompt-actions #send-prompt-btn {
  color: #fff;
  display: none;
  background: #1d7efd;
}
.prompt-form .prompt-input:valid~.prompt-actions #send-prompt-btn {
  display: block;
}
.prompt-form #send-prompt-btn:hover {
  background: #0264e3;
}
.prompt-form .file-upload-wrapper :where(button, img) {
  display: none;
  border-radius: 50%;
  object-fit: cover;
  position: absolute;
}
.prompt-form .file-upload-wrapper.active #add-file-btn {
  display: none;
}
.prompt-form .file-upload-wrapper #add-file-btn,
.prompt-form .file-upload-wrapper.active.img-attached img,
.prompt-form .file-upload-wrapper.active.file-attached .file-icon,
.prompt-form .file-upload-wrapper.active:hover #cancel-file-btn {
  display: block;
}
.prompt-form :is(#stop-response-btn:hover, #cancel-file-btn) {
  color: #d62939;
}
.prompt-wrapper .prompt-form .file-icon {
  color: #1d7efd;
}
.prompt-form #stop-response-btn,
body.bot-responding .prompt-form .file-upload-wrapper {
  display: none;
}
body.bot-responding .prompt-form #stop-response-btn {
  display: block;
}
.prompt-container .disclaimer-text {
  font-size: 0.9rem;
  text-align: center;
  padding: 16px 20px 0;
  color: var(--placeholder-color);
}
/* Responsive media query code for small screens */
@media (max-width: 768px) {
  .container {
    padding: 20px 0 100px;
  }
  .app-header :is(.heading, .sub-heading) {
    font-size: 2rem;
    line-height: 1.4;
  }
  .app-header .sub-heading {
    font-size: 1.7rem;
  }
  .container .chats-container {
    gap: 15px;
  }
  .chats-container .bot-message {
    margin: 4px auto;
  }
  .prompt-container :where(.prompt-wrapper, .prompt-form, .prompt-actions) {
    gap: 8px;
    height: 53px;
  }
  .prompt-container button {
    width: 53px;
  }
  .prompt-form :is(.file-upload-wrapper, button, img) {
    height: 42px;
    width: 42px;
  }
  .prompt-form .prompt-input {
    padding-left: 20px;
  }
  .prompt-form .file-upload-wrapper.active #cancel-file-btn {
    opacity: 0;
  }
  .prompt-wrapper.hide-controls :where(#theme-toggle-btn, #delete-chats-btn) {
    display: none;
  }
}

Step 4: Frontend JS — Fully Functional Chatbot

Use this JS code:


// Selectors
const container = document.querySelector(".container");
const chatsContainer = document.querySelector(".chats-container");
const promptForm = document.querySelector(".prompt-form");
const promptInput = promptForm.querySelector(".prompt-input");
const fileInput = promptForm.querySelector("#file-input");
const fileUploadWrapper = promptForm.querySelector(".file-upload-wrapper");
const themeToggleBtn = document.querySelector("#theme-toggle-btn");
const stopResponseBtn = document.querySelector("#stop-response-btn");
const deleteChatsBtn = document.querySelector("#delete-chats-btn");

// Replace with your OpenAI API key and endpoint
const API_KEY = "PASTE-YOUR-API-KEY";
const API_URL = "https://api.openai.com/v1/chat/completions";

let controller;
let typingInterval;
const chatHistory = [];
const userData = { message: "", file: {} };

// Initialize theme
const isLightTheme = localStorage.getItem("themeColor") === "light_mode";
document.body.classList.toggle("light-theme", isLightTheme);
themeToggleBtn.textContent = isLightTheme ? "dark_mode" : "light_mode";

// Helpers
const scrollToBottom = () =>
  container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });

const createMessageElement = (content, ...classes) => {
  const div = document.createElement("div");
  div.classList.add("message", ...classes);
  div.innerHTML = content;
  return div;
};

const typingEffect = (text, textElement, botMsgDiv) => {
  textElement.textContent = "";
  let index = 0;
  typingInterval = setInterval(() => {
    if (index < text.length) {
      textElement.textContent += text[index++];
      scrollToBottom();
    } else {
      clearInterval(typingInterval);
      botMsgDiv.classList.remove("loading");
      document.body.classList.remove("bot-responding");
    }
  }, 20);
};

// API call
const generateResponse = async (botMsgDiv) => {
  const textElement = botMsgDiv.querySelector(".message-text");
  controller = new AbortController();

  chatHistory.push({
    role: "user",
    content: userData.message,
  });

  try {
    const response = await fetch(API_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_KEY}`,
      },
      body: JSON.stringify({
        model: "gpt-4o-mini",
        messages: chatHistory,
        max_tokens: 800,
      }),
      signal: controller.signal,
    });

    const data = await response.json();
    if (!response.ok) throw new Error(data.error?.message || "API error");

    const botText = data.choices[0].message.content;
    typingEffect(botText, textElement, botMsgDiv);

    chatHistory.push({ role: "assistant", content: botText });
  } catch (err) {
    textElement.textContent =
      err.name === "AbortError" ? "Response stopped." : err.message;
    textElement.style.color = "#d62939";
    botMsgDiv.classList.remove("loading");
    document.body.classList.remove("bot-responding");
    scrollToBottom();
  } finally {
    userData.file = {};
  }
};

// Handle form submit
const handleFormSubmit = (e) => {
  e.preventDefault();
  const message = promptInput.value.trim();
  if (!message || document.body.classList.contains("bot-responding")) return;

  userData.message = message;
  promptInput.value = "";
  document.body.classList.add("chats-active", "bot-responding");
  fileUploadWrapper.classList.remove("file-attached", "img-attached", "active");

  // User message
  const userHTML = `
    

${ userData.file.data ? userData.file.isImage ? `` : `

description${userData.file.fileName}

` : "" } `; const userMsgDiv = createMessageElement(userHTML, "user-message"); userMsgDiv.querySelector(".message-text").textContent = message; chatsContainer.appendChild(userMsgDiv); scrollToBottom(); // Bot placeholder setTimeout(() => { const botHTML = `

Just a sec...

`; const botMsgDiv = createMessageElement(botHTML, "bot-message", "loading"); chatsContainer.appendChild(botMsgDiv); scrollToBottom(); generateResponse(botMsgDiv); }, 500); }; // File input fileInput.addEventListener("change", () => { const file = fileInput.files[0]; if (!file) return; const isImage = file.type.startsWith("image/"); const reader = new FileReader(); reader.onload = (e) => { const base64 = e.target.result.split(",")[1]; fileUploadWrapper.querySelector(".file-preview").src = e.target.result; fileUploadWrapper.classList.add("active", isImage ? "img-attached" : "file-attached"); userData.file = { fileName: file.name, data: base64, mime_type: file.type, isImage }; }; reader.readAsDataURL(file); }); // Cancel file document.querySelector("#cancel-file-btn").addEventListener("click", () => { userData.file = {}; fileUploadWrapper.classList.remove("file-attached", "img-attached", "active"); }); // Stop bot stopResponseBtn.addEventListener("click", () => { controller?.abort(); userData.file = {}; clearInterval(typingInterval); const botMsg = chatsContainer.querySelector(".bot-message.loading"); botMsg?.classList.remove("loading"); document.body.classList.remove("bot-responding"); }); // Theme toggle themeToggleBtn.addEventListener("click", () => { const isLight = document.body.classList.toggle("light-theme"); localStorage.setItem("themeColor", isLight ? "light_mode" : "dark_mode"); themeToggleBtn.textContent = isLight ? "dark_mode" : "light_mode"; }); // Delete chats deleteChatsBtn.addEventListener("click", () => { chatHistory.length = 0; chatsContainer.innerHTML = ""; document.body.classList.remove("chats-active", "bot-responding"); }); // Suggestions click document.querySelectorAll(".suggestions-item").forEach((item) => item.addEventListener("click", () => { promptInput.value = item.querySelector(".text").textContent; promptForm.dispatchEvent(new Event("submit")); }) ); // Add event listeners promptForm.addEventListener("submit", handleFormSubmit); promptForm.querySelector("#add-file-btn").addEventListener("click", () => fileInput.click());

Info!
Make sure to replace API_KEY with your OpenAI API key. The frontend will communicate with OpenAI API directly or via a backend proxy.

View Demo
OpenAI Chatbot v1.0.zip 45.4 KiB

Step 5: Optional Backend Proxy (Secure)

To avoid exposing your API key, create a Node.js server:


import express from "express";
import fetch from "node-fetch";
import dotenv from "dotenv";
dotenv.config();

const app = express();
app.use(express.json());

const OPENAI_KEY = process.env.OPENAI_API_KEY;

app.post("/api/chat", async (req, res) => {
  const { messages } = req.body;
  try {
    const response = await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${OPENAI_KEY}`,
      },
      body: JSON.stringify({
        model: "gpt-4o-mini",
        messages,
        max_tokens: 800,
      }),
    });
    const data = await response.json();
    res.status(response.status).json(data);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000, () => console.log("Server running on port 3000"));

Warning!
Never commit your API key to public repos. Use environment variables for security.

Step 6: Deployment

You can deploy worldwide using:

PlatformUse-caseEase
VercelFrontend + serverless APIVery Easy
Fly.ioDocker container + multi-regionModerate
Netlify / RenderFrontend + serverless APIEasy

Step 7: Production Tips

  1. Rate-limit requests to prevent overuse of API tokens.
  2. Use multi-region deployment for global latency improvement.
  3. Cache common responses to reduce API calls.
  4. Sanitize user input and outputs.
  5. Log minimal metadata to protect privacy.

Step 8: Example Use-Cases

  • Customer support assistant
  • Code helper / IDE assistant
  • FAQ assistant for websites
  • Multilingual chatbot with locale-specific prompts
Success! Your chatbot is now fully functional. Users only need to insert the OpenAI API key, and it’s ready to run, chat, upload files, toggle themes, and deploy worldwide!

Resources

OpenAI Chat API docs
Vercel Edge Functions
Fly.io Docker deployment
1. Can I use this chatbot without adding my own API key?

No. You must provide your OpenAI API key in the designated variable. This ensures requests are authenticated and secure. Never share your key publicly.

2. Does this chatbot support file uploads?

Yes. Users can upload images or text files. The chatbot can process them and include the data in its responses. Files are handled securely in the frontend and passed to the API via your server.

3. Can I enable both light and dark themes?

Absolutely. The chatbot comes with a theme toggle button. User preference is saved in local storage so the theme persists across sessions.

4. How can I deploy this chatbot globally?

You can deploy using platforms like Vercel (serverless + edge functions) for fast global access, or Fly.io for container-based multi-region deployments. Both allow low-latency access worldwide.

5. How do I handle API usage and costs?

Monitor token usage using OpenAI’s dashboard. Implement server-side rate limiting and user quotas to prevent overspending. Use smaller models for low-cost queries and reserve larger models for high-value responses.

Post a Comment