Building a Chatbot
Build a conversational chatbot with multi-turn memory using the Conduit.im API, step by step.
What You'll Build
By the end of this guide you'll have a working chatbot that:
- Sends and receives messages via the Chat Completions API
- Maintains conversation history for multi-turn dialogue
- Uses a system prompt to control the assistant's behaviour
- Streams responses for a real-time typing effect
Prerequisites
- A Conduit.im account with an API key
- Node.js 18 or later installed
- Basic familiarity with JavaScript and REST APIs
Step 1: Project Setup
Create a new directory and initialise a Node.js project:
mkdir conduit-chatbot && cd conduit-chatbot
npm init -yStore your API key in an environment variable so it stays out of source code:
export CONDUIT_API_KEY="your-api-key-here"Tip: In production, use a .env file or a secrets manager instead of exporting directly in the shell.
Step 2: Send Your First Message
Create a file called chat.mjs with a simple function that calls the Chat Completions API:
// chat.mjs
const API_URL = "https://api.conduit.im/v1/chat/completions";
const API_KEY = process.env.CONDUIT_API_KEY;
async function chat(userMessage) {
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4",
messages: [
{ role: "user", content: userMessage },
],
}),
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error?.message || "Request failed");
}
const data = await response.json();
return data.choices[0].message.content;
}
// Try it out
const reply = await chat("What is an API gateway?");
console.log(reply);Run it:
node chat.mjsYou should see the model's answer printed to the console. This is a one-shot request — the model has no memory of previous messages. Next we'll fix that.
Step 3: Add Conversation History
To make the chatbot remember context, keep a running array of messages and send the full history with every request. Add a system prompt to shape the assistant's personality.
// chatbot.mjs
import * as readline from "node:readline/promises";
const API_URL = "https://api.conduit.im/v1/chat/completions";
const API_KEY = process.env.CONDUIT_API_KEY;
// Conversation history — the system message sets the tone
const messages = [
{
role: "system",
content:
"You are a friendly and concise coding assistant. " +
"Answer questions clearly and include short code examples when helpful.",
},
];
async function sendMessage(userMessage) {
messages.push({ role: "user", content: userMessage });
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4",
messages, // send the full history
max_tokens: 1024,
}),
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error?.message || "Request failed");
}
const data = await response.json();
const reply = data.choices[0].message.content;
// Append the assistant's reply so the next call has it
messages.push({ role: "assistant", content: reply });
return reply;
}
// Interactive loop
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log('Chatbot ready — type "exit" to quit.\n');
while (true) {
const input = await rl.question("You: ");
if (input.toLowerCase() === "exit") break;
const reply = await sendMessage(input);
console.log(`\nAssistant: ${reply}\n`);
}
rl.close();Run node chatbot.mjs and try a multi-turn conversation. The bot now remembers what you said earlier in the session.
How Conversation Memory Works
The Chat Completions API is stateless — the server does not store your conversation. Memory is achieved by sending the full message array each time:
// Turn 1 — one user message
{ "messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What is Node.js?" }
]}
// Turn 2 — previous exchange included
{ "messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What is Node.js?" },
{ "role": "assistant", "content": "Node.js is a JavaScript runtime..." },
{ "role": "user", "content": "How do I install it?" }
]}Keep in mind: Every token in the message history counts towards your usage. For long conversations, consider trimming older messages or summarising them to control costs.
Step 4: Stream Responses
Streaming lets you display tokens as they arrive instead of waiting for the full response. Set "stream": true and process server-sent events:
async function sendMessageStreaming(userMessage) {
messages.push({ role: "user", content: userMessage });
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4",
messages,
stream: true,
}),
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error?.message || "Request failed");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullReply = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
// Each SSE chunk is prefixed with "data: "
for (const line of chunk.split("\n")) {
if (!line.startsWith("data: ")) continue;
const payload = line.slice(6);
if (payload === "[DONE]") break;
const parsed = JSON.parse(payload);
const token = parsed.choices[0]?.delta?.content || "";
process.stdout.write(token);
fullReply += token;
}
}
console.log(); // newline after streaming finishes
messages.push({ role: "assistant", content: fullReply });
return fullReply;
}Replace the sendMessage call in your interactive loop with sendMessageStreaming to see tokens appear in real time.
Step 5: Switch Models Easily
One of Conduit.im's key benefits is switching between models with a single parameter change. No SDK swaps, no credential changes — just update the model field:
// Try different models by changing this value
const MODEL = "claude-3-sonnet"; // or "gpt-4", "gemini-pro", etc.
body: JSON.stringify({
model: MODEL,
messages,
max_tokens: 1024,
})Browse the Models page to see every model available through the API, including pricing and context window sizes.
Error Handling
Production chatbots should handle errors gracefully. Here are the most common scenarios:
| Status | Cause | Recommended Action |
|---|---|---|
| 401 | Invalid or missing API key | Check your key and environment variable |
| 402 | Insufficient balance | Add credits on the billing page |
| 429 | Rate limit exceeded | Wait and retry with exponential back-off |
| 500 | Server or upstream error | Retry after a short delay |
See the Error Handling reference for the full list of error codes.
Production Tips
Trim conversation history
Long histories increase token usage and cost. Keep a sliding window of the most recent messages, or summarise older turns into a single system message.
Set spending limits
Configure per-key spending limits so a runaway loop can't drain your balance.
Use streaming in user-facing apps
Streaming dramatically improves perceived latency. Users see the first token within milliseconds instead of waiting several seconds for the full response.
Store conversations server-side
Never send API requests directly from the browser. Proxy through your own server so the API key stays secret and you can log conversations for debugging.
Next Steps
You now have a working chatbot with conversation memory and streaming. Here's where to go next: