Building a Real-Time Chat App with the Claude API
Building a real-time chat application with the Claude API means streaming tokens to the browser as they're generated, rather than waiting for a full response and dumping it on screen at once. The core building blocks are: a streaming endpoint that emits partial message chunks, a frontend that renders those chunks incrementally, and a conversation state layer that keeps track of message history between turns.
This is different from a typical "call an API, get JSON back" integration. Chat feels real-time when the first token appears in under a second and text keeps flowing continuously — like watching someone type. Getting this right requires understanding Claude's streaming format, handling reconnects and errors gracefully, and managing conversation history so each request includes the right context. Below is a practical walkthrough of the architecture and code.
Core Architecture
A real-time Claude chat app typically has three layers:
- Frontend — renders an input box, a message list, and appends streamed tokens to the last assistant message as they arrive.
- Backend/API layer — holds the API key, forwards requests to Claude, and streams the response back to the client (usually over Server-Sent Events or a WebSocket).
- Conversation store — keeps the message array (
user/assistantturns) so context persists across the session, either in memory, a database, or client-side state for short sessions.
Never call the Claude API directly from the browser — this exposes your API key. Always proxy through a backend, even a thin serverless function.
Streaming Responses from Claude
Claude's Messages API supports streaming via server-sent events when you set stream: true. Your backend receives a sequence of events (content_block_delta, message_stop, etc.) and should forward the text deltas to the client as they arrive.
A minimal Node.js proxy pattern looks like this:
app.post("/chat", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
const upstream = await fetch("https://api.example.com/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-model",
messages: req.body.messages,
stream: true
})
});
for await (const chunk of upstream.body) {
res.write(chunk); // forward raw SSE chunks
}
res.end();
});
The frontend consumes this with EventSource or a fetch + ReadableStream reader, appending each delta to the current assistant message in the UI.
If you're using SubToAPI as your Claude access layer, the same streaming pattern applies against a single, stable endpoint:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet",
"max_tokens": 1024,
"stream": true,
"messages": [
{"role": "user", "content": "Explain WebSockets in one sentence."}
]
}'
The application key format (sub_live_...) means you can rotate keys per environment or per app without touching your Claude account settings, which matters once you have a staging environment and a production chat app both hitting the same model. Full details on streaming events are in the streaming docs.
Managing Conversation State
Real-time chat isn't just about streaming — it's about maintaining context. Claude's API is stateless: every request must include the full message history you want the model to see. For a chat app this means:
- Store each turn (
role: "user"/role: "assistant") in an array as the conversation progresses. - On each new user message, append it to the array and send the whole thing.
- Once the assistant's streamed response finishes, append it as a new
assistantmessage before the next turn. - Trim or summarize history once you approach the model's context window, especially for long-running support or coding chat sessions.
let messages = [];
function sendMessage(userText) {
messages.push({ role: "user", content: userText });
streamChatResponse(messages).then((fullReply) => {
messages.push({ role: "assistant", content: fullReply });
});
}
If multiple users or sessions share your backend, key the message history by session ID or user ID, and persist it in Redis or a database if the chat needs to survive page reloads or server restarts.
Handling Errors and Reconnects
Real-time connections drop. A production-ready chat app needs:
- Timeouts — if no token arrives within a few seconds of connecting, show a retry option instead of a stuck spinner.
- Partial message recovery — if the stream cuts off mid-response, either resend the request or clearly mark the message as incomplete rather than pretending it finished.
- Rate limit handling — surface a friendly "please wait" message instead of a raw 429 error, and consider client-side debouncing so users can't fire five messages in two seconds.
- Tool use in-stream — if your chat app uses function calling (search, calculators, database lookups), you need to detect tool-use blocks in the stream and pause rendering until the tool result comes back. See the tools docs for the message shapes involved.
Where SubToAPI Fits
If you're prototyping or shipping a chat product on top of Claude, the operational overhead — managing API keys, tracking usage across environments, giving teammates scoped access — adds up fast. SubToAPI wraps your existing Claude access in a standard HTTPS API with per-application sub_live_... keys, streaming support, tool use, and usage metadata in one dashboard, so you're not building key rotation and usage tracking yourself before you've even shipped the chat UI. Plans start at €9/month for solo builders, with team and scale tiers for shared workspaces — see pricing or start with the quickstart guide.
Testing Your Real-Time Chat Locally
Before wiring up a full frontend, test the streaming behavior directly with curl to confirm chunks arrive incrementally:
curl -N https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet","max_tokens":256,"stream":true,"messages":[{"role":"user","content":"Count to 5 slowly."}]}'
The -N flag disables curl's output buffering so you see chunks as they arrive, matching what your browser will see. This is the fastest way to debug whether a "stuck" chat UI is a frontend rendering bug or a backend streaming issue.
questions
Do I need WebSockets to stream Claude responses in real time? No. Server-Sent Events (SSE) over a standard HTTP connection are sufficient for one-directional streaming from server to client, which covers most chat UIs. WebSockets add value mainly for bidirectional features like typing indicators or multi-user chat rooms.
How do I keep conversation context across multiple messages? Claude's API is stateless, so you must resend the full message history (or a trimmed/summarized version) with every request. Store the array of user/assistant turns server-side or client-side and append to it after each exchange.
Can I build a real-time Claude chat app without managing raw API keys myself? Yes — using a service like SubToAPI, you get a hosted HTTPS endpoint with application-scoped keys, streaming, and usage tracking already built in, so your chat backend just calls a standard REST endpoint instead of managing Claude credentials directly. Check the messages docs for request/response details.