A chatbot is four moving parts: a web UI, a backend that holds the API key, one HTTPS call to an AI API, and the conversation history you resend with every turn. This topic walks the loop with fetch() against OpenAI-compatible endpoints — system instructions, 429 error handling, and the security rules that keep your key out of the browser.
Modern AI APIs are plain HTTPS: you POST a JSON body listing the conversation so far — [{ role: "system", … }, { role: "user", … }, { role: "assistant", … }] — and receive the next assistant message. OpenAI (api.openai.com/v1/chat/completions), Azure OpenAI, Anthropic and local servers like Ollama follow this request/response shape, so the skill transfers everywhere. The one architecture rule that matters: the browser talks only to YOUR backend; the backend holds the API key in an environment variable and calls the AI API server-side. A key pasted into front-end JavaScript is public within minutes.
Every AI feature you will ever ship — support bot, ticket summarizer, log explainer, agent — starts with this exact loop. Learn it once against one endpoint and you can swap providers by changing a base URL and a model name. It also teaches the disciplines that separate professional work from demos: secrets handling, stateless request design, rate-limit retries and honest error messages. And it is the base on which topics 16-20 (agents, tool calling, memory, security, the capstone) are built — none of them work without a solid request/response loop.
The round trip has five beats. 1) User types a message in the web UI. 2) The UI POSTs it to your backend route (e.g. /api/chat) — never directly to the AI provider. 3) The backend loads the key from process.env, attaches the fixed system instruction, appends the conversation history, and calls the chat/completions endpoint. 4) The API replies with JSON; the reply text sits at data.choices[0].message.content (OpenAI shape). 5) The backend appends the reply to stored history with role "assistant" and returns it to the UI. The API is stateless — it remembers nothing — so step 3 resending history is what makes the conversation feel continuous.
User
↓ types a message
Web UI (React page)
↓ POST /api/chat
Backend (Next.js route)
↓ chat/completions + API key
AI API (OpenAI / Azure / Ollama)
↓ JSON response
Backend → Response → Web UIA first chatbot for an IT team usually becomes an internal Q&A widget: employees ask “how do I connect to VPN?”, the backend adds a system instruction (“answer as the IT helpdesk; link to runbooks; never invent policy”), and conversation history keeps follow-ups coherent. The same loop powers Slack bots (a channel app receiving events and replying via chat.postMessage), Teams support bots, and CloudEngineerHub’s /rca analyzer UI — which streams staged results to the user instead of returning one blocking response.
Create an API key in the OpenAI console (or deploy Azure OpenAI, or run Ollama locally at http://localhost:11434). Put it in .env.local as OPENAI_API_KEY — environment variables only; never in client-side code, never committed to git.
In Next.js 16 add a route handler that reads the key from process.env and forwards the conversation. It must validate input (cap message length), attach the system instruction server-side, and rate-limit per user so the client can never override your assistant’s rules.
The request body (see the command block for the full JSON): POST https://api.openai.com/v1/chat/completions { "model": "gpt-4o-mini", "temperature": 0.3, "messages": [ { "role": "system", "content": "You are an Intune support assistant." }, { "role": "user", "content": "What causes error 0x87D1041C?" } ] } Backend call: const res = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + process.env.OPENAI_API_KEY }, body: JSON.stringify(payload) });
The reply is at data.choices[0].message.content (OpenAI shape). Append it to history with role "assistant" and render it in the UI. Also read data.usage — total_tokens is your cost meter.
The API is stateless: every request must include the full messages array — system, user and assistant turns. Trim to the last ~10 messages or summarize older ones to control tokens and cost; a bot that sends only the latest message forgets the topic every turn.
HTTP 429 = rate limited → retry with exponential backoff (1s, 2s, 4s) and honour Retry-After; 401 = bad key; 5xx = upstream problem. Wrap the call in try/catch with a timeout, show a friendly message in the UI, and never leak stack traces or the key to the client.
curl https://api.openai.com/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer $OPENAI_API_KEY" -d '{"model":"gpt-4o-mini","temperature":0.3,"messages":[{"role":"system","content":"You are an Intune support assistant."},{"role":"user","content":"What causes error 0x87D1041C?"}]}'Three rules carry you far. First, keys live in environment variables (or Azure Key Vault in production) and are used only server-side — never in React components, git history or client bundles; rotate keys on a schedule and treat any leak as compromised. Second, validate and rate-limit the backend route: cap message length, cap requests per user per hour, and strip user-supplied "system" roles so clients cannot rewrite instructions. Third, treat model output as untrusted text: render it as plain text (not HTML) to avoid XSS through malicious content, and never auto-execute commands it suggests.
Anything in front-end JavaScript is readable by the user, so an API key shipped to the browser is compromised the moment the page loads — and it will be scraped and abused. The backend keeps the key in an environment variable and applies validation, rate limiting and the fixed system instruction, returning only the model’s text. This is the standard pattern for every serious chatbot.
It does not remember; the application resends. Every request carries the full messages array — system, user and assistant turns — so the model sees the conversation as one prompt. Production apps trim or summarize older turns to bound tokens and cost, and store history in a database keyed to the user session.
429 means you exceeded requests-per-minute or tokens-per-minute. Retry with exponential backoff — 1s, 2s, 4s — honour the Retry-After header when present, add jitter so parallel clients do not stampede, and queue rather than drop when possible. Also read the rate-limit response headers to throttle proactively before hitting the wall.
Build /api/chat in a Next.js app: environment-variable key, fixed system instruction (“IT helpdesk assistant”), 4,000-character message cap, 429 retry with exponential backoff (max 3), and a React page with a message list + input. Then test the failure paths deliberately: bad key (401), a flood of requests (429), and a very long message.
Any OpenAI-compatible endpoint: OpenAI directly, Azure OpenAI when you need enterprise compliance inside your Azure tenant, or Ollama (http://localhost:11434) for free local experimentation with models like Llama 3.1. The request shape is nearly identical, so switching later is a base-URL and model-name change.
With gpt-4o-mini, a 1,000-token conversation costs well under a cent — a 10,000-conversation month lands in single-digit dollars. Cost control is conversation hygiene: send history selectively, cap max_tokens, and route simple questions to the mini model.
No. A first chatbot is one fetch() call plus message-array management — a framework hides exactly the parts you should learn first. Add orchestration libraries later, when you have retrieval, tools or agents (topics 16-17), and only if their abstractions pay for themselves.