How to Develop a Chatbot with Claude Code
Learn how to develop a chatbot using Claude Code. Includes practical code examples and step-by-step guidance.
Developing a Chatbot With Claude Code
Building an AI chatbot involves a lot of moving parts: UI, API integration, conversation management, streaming, and more. With Claude Code, you can pull all of these together into a working chatbot in a short amount of time.
Implementing a Basic Chat UI
> Create a React component for a chatbot.
> Support streaming responses, conversation history display, and an input form.
import { useState, useRef, useEffect } from "react";
interface Message {
id: string;
role: "user" | "assistant";
content: string;
}
export function ChatBot() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const sendMessage = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = {
id: crypto.randomUUID(),
role: "user",
content: input,
};
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
const assistantId = crypto.randomUUID();
setMessages((prev) => [
...prev,
{ id: assistantId, role: "assistant", content: "" },
]);
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [...messages, userMessage].map(({ role, content }) => ({
role,
content,
})),
}),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId
? { ...m, content: m.content + chunk }
: m
)
);
}
} catch (error) {
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId
? { ...m, content: "An error occurred. Please try again." }
: m
)
);
}
setIsLoading(false);
};
return (
<div className="flex flex-col h-[600px] border rounded-lg">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${
msg.role === "user"
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-900"
}`}
>
{msg.content}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="border-t p-4 flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && sendMessage()}
placeholder="Type a message..."
className="flex-1 p-2 border rounded-lg"
disabled={isLoading}
/>
<button
onClick={sendMessage}
disabled={isLoading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
Send
</button>
</div>
</div>
);
}
A Streaming API Route
An API route that calls the Anthropic API on the backend and streams the response back.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = await client.messages.stream({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: "You are a helpful, polite assistant. Please answer clearly and concisely.",
messages,
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
controller.enqueue(encoder.encode(event.delta.text));
}
}
controller.close();
},
});
return new Response(readable, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
Persisting Conversation History
Save the conversation to a database so the user can resume it later.
import { db } from "@/lib/database";
export async function saveConversation(
userId: string,
messages: Message[]
) {
return db.conversation.upsert({
where: { id: `${userId}-current` },
update: {
messages: JSON.stringify(messages),
updatedAt: new Date(),
},
create: {
id: `${userId}-current`,
userId,
messages: JSON.stringify(messages),
},
});
}
export async function loadConversation(userId: string): Promise<Message[]> {
const conv = await db.conversation.findUnique({
where: { id: `${userId}-current` },
});
return conv ? JSON.parse(conv.messages as string) : [];
}
Adding RAG (Retrieval-Augmented Generation)
If you want a chatbot that answers based on internal documents, a RAG setup is a great fit.
import { searchDocuments } from "@/lib/vector-search";
async function generateRAGResponse(query: string, conversationHistory: Message[]) {
// Search related documents
const relevantDocs = await searchDocuments(query, { limit: 5 });
const context = relevantDocs
.map((doc) => `---\n${doc.title}\n${doc.content}\n---`)
.join("\n");
const systemPrompt = `Use the documents below to answer the question.
If the information is not in the documents, reply with "I couldn't find that information."
${context}`;
return client.messages.stream({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: systemPrompt,
messages: conversationHistory,
});
}
For extending features via an MCP server, see the MCP server guide. For effective prompt design, see 5 tips for better prompts.
Summary
With Claude Code, you can efficiently develop a chatbot that includes a chat UI, streaming API, conversation management, and a RAG setup. An incremental approach where you add features step by step works particularly well.
For details, see the official Claude Code documentation and the Anthropic API reference.
2026 Production Upgrade
A chatbot is a small application that accepts a user’s message, keeps enough context to understand the conversation, and returns the next useful response. In plain terms, it is a conversational front door for support, sales, onboarding, search, or internal operations. The hard part is not drawing the chat window. The hard part is deciding what the bot is allowed to do, where it should admit uncertainty, and how it hands the user to a safer workflow when automation is not enough.
Claude Code is useful for this work because the product touches several layers at once: React UI, backend API routes, streaming, persistence, retrieval, analytics, and deployment checks. A thin demo can be built quickly, but a production chatbot needs a narrower promise. For the first release, choose one job: answer the top support questions, qualify inbound leads, search internal documentation, or collect incident details. A focused bot is easier to test, cheaper to run, and more trustworthy than a general assistant that tries to answer everything.
The monetization angle depends on that focus. A support bot lowers human workload. A sales bot increases qualified consultations. A learning bot guides readers toward a course, template, or training offer. A documentation bot keeps existing users successful so they do not churn. Each of those goals needs a different success metric, so define the metric before you write the final prompts.
Architecture Table
| Layer | Responsibility | Production note |
|---|---|---|
| React chat UI | Render messages, input state, loading state, and retry affordances | Start with predictable state from React hooks; see the React useState docs |
| API route | Keep API keys server-side and normalize requests | Add input limits, authentication, and request timeouts before public launch |
| Streaming response | Return partial text so users do not stare at a blank screen | Use Web Streams carefully; the MDN Streams API guide explains the primitives |
| Conversation store | Resume conversations and audit failures | Store only what you need, redact private data, and define deletion behavior |
| Retrieval layer | Search product docs, policies, or knowledge base pages | If retrieval finds weak evidence, the bot should say so instead of inventing |
| Webhook layer | Send qualified events to CRM, ticketing, or Slack | Pair this with Claude Code webhook implementation |
| Analytics layer | Measure answer success, escalations, and CTA clicks | Use Claude Code analytics implementation to close the loop |
For the API contract itself, keep the request shape close to the examples in Claude Code API development: an array of role/content messages, a server-side system instruction, and a narrow response stream. That makes it easier to swap the UI, add mobile clients, or introduce a second model later.
Runnable Streaming Demo
The following file runs without external services. Save it as chatbot-stream-demo.mjs and run node chatbot-stream-demo.mjs. It does not call Claude; it proves the streaming and history mechanics before you attach the real API route.
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const faq = new Map([
["password", "Open the account page, choose Reset password, and follow the email link."],
["pricing", "The pricing page explains plans. For a custom quote, collect team size and required features."],
["refund", "Refund requests should be routed to support with the order id and purchase email."],
]);
const history = [];
function chooseAnswer(question) {
const normalized = question.toLowerCase();
for (const [keyword, answer] of faq) {
if (normalized.includes(keyword)) return answer;
}
return "I could not find a safe answer in the FAQ. I will hand this to a human operator.";
}
async function* streamText(text) {
for (const token of text.split(/(\s+)/)) {
await new Promise((resolve) => setTimeout(resolve, 15));
yield encoder.encode(token);
}
}
async function ask(question) {
history.push({ role: "user", content: question });
const answer = chooseAnswer(question);
process.stdout.write(`\nUser: ${question}\nAssistant: `);
let fullAnswer = "";
for await (const chunk of streamText(answer)) {
const token = decoder.decode(chunk);
fullAnswer += token;
process.stdout.write(token);
}
history.push({ role: "assistant", content: fullAnswer });
}
await ask("How do I reset my password?");
await ask("Can I see pricing before talking to sales?");
console.log(`\n\nSaved ${history.length} messages.`);
In production, replace chooseAnswer with your Claude API call and keep the rest of the shape: push the user message, stream assistant output, then save the final assistant text. This sequence prevents a common failure where the UI shows partial text but the database stores an empty answer because persistence ran too early.
Real Use Cases
The first use case is lead qualification for a SaaS product. The chatbot answers pricing, security, integration, and procurement questions, then routes high-intent visitors to a consultation form. The goal is not to replace sales. The goal is to remove repetitive first-touch questions so humans spend time on qualified conversations.
The second use case is internal help desk automation. Employees ask about expenses, leave requests, VPN access, device replacement, or onboarding steps. A retrieval layer can cite the exact policy page, while a webhook can open a ticket when the answer requires approval. This is a strong workflow because the content is bounded and the fallback path is clear.
The third use case is a learning or content site. A reader asks whether they should learn API integration, webhooks, or analytics next. The bot can recommend Claude Code API development, Claude Code webhook implementation, or Claude Code analytics implementation, then invite the reader to deeper training. That turns search intent into an owned monetization path without interrupting the article.
The fourth use case is incident intake. Instead of asking a user to fill a long form, the bot gathers the error message, timestamp, browser, account id, and reproduction steps. Once the minimum fields are present, it creates a support ticket. Here, success means a complete handoff, not an instant answer.
Pitfalls and Failure Modes
Do not send unlimited conversation history to the model. Long histories increase cost, slow the response, and can let an old instruction override the current task. Summarize older turns, keep the latest decision points, and avoid storing sensitive data that is not needed for support or analytics.
Do not let retrieval failures become confident answers. If the knowledge base search returns weak matches, the bot should say it cannot verify the answer and offer a human path. This one rule prevents many hallucination-driven support problems.
Do not treat streaming as a visual flourish. Streaming creates new failure modes: partial answers after a network drop, duplicate sends, abandoned requests, and UI state that never leaves “loading”. Use disabled submit buttons, request cancellation, timeouts, and an explicit retry message.
Do not publish without measurement. Track unresolved questions, escalation rate, CTA clicks, and the first message that led to a consultation or purchase. If you only count total messages, a noisy bot can look successful while it is actually frustrating users.
Monetization CTA
For a reader-facing site, the CTA should match the conversation stage. A beginner might need a checklist or starter template. A team lead might need an implementation review. A company evaluating automation might need a short consultation. ClaudeCodeLab routes that commercial intent through training, where the next step can be a hands-on session rather than another generic article.
The practical content path is: teach the concept here, explain the API in Claude Code API development, connect downstream systems with webhooks, and improve conversion with analytics. That sequence makes the article useful for readers and creates a credible monetization journey.
What I Verified
I verified the runnable demo with Node.js 20 using only standard TextEncoder, TextDecoder, async generators, and top-level await. For a real app, I would test the React state flow first, then connect the API route, then add persistence and analytics. That order keeps debugging small: if streaming breaks, you know whether the bug is in the UI, the route, or the model call.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.