Claude API Real-Time Chat Application Example
If you're searching for a Claude API real-time chat application example, you want working code that streams tokens to the browser as they're generated, not a wall of text that appears all at once after a 10-second wait. This article walks through a complete, minimal chat app: a backend endpoint that calls Claude, a frontend that renders tokens as they arrive, and the message-history logic that makes it feel like an actual conversation instead of a series of disconnected questions.
The core pieces you need are: a server-side call to the Claude Messages API with stream: true, a way to pass that stream through to the client (Server-Sent Events or a simple fetch with a readable stream), and client-side state that appends each chunk to the current message as it arrives. Below is a working version you can adapt.
Backend: streaming endpoint
This example uses Node.js with Express, but the pattern is the same in any backend language. The key is opening the connection to Claude with streaming enabled and forwarding each chunk to the client immediately.
app.post('/api/chat', async (req, res) => {
const { messages } = req.body;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
messages,
stream: true
})
});
for await (const chunk of response.body) {
res.write(chunk);
}
res.end();
});
This forwards Anthropic's SSE stream directly to the browser. Each event contains a delta with a small piece of text, which you accumulate on the client.
Frontend: rendering tokens as they arrive
On the client, you read the response stream and parse each content_block_delta event, appending the text to the current assistant message in your UI state.
async function sendMessage(messages) {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages })
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let assistantText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(l => l.startsWith('data: '));
for (const line of lines) {
const data = JSON.parse(line.replace('data: ', ''));
if (data.type === 'content_block_delta') {
assistantText += data.delta.text;
updateChatUI(assistantText); // repaint the last message bubble
}
}
}
}
updateChatUI is your own function that updates the DOM or framework state — in React this is typically a setState call that updates the last message in an array on every delta.
Managing conversation history
A real chat application needs to send prior turns back to Claude on every request, since the API is stateless. Keep an array of {role, content} objects in your frontend state and append both the user's message and the completed assistant response after each round trip:
const messages = [
{ role: 'user', content: 'What's the capital of Portugal?' },
{ role: 'assistant', content: 'Lisbon.' },
{ role: 'user', content: 'And its population?' }
];
Watch your token budget as history grows — long conversations eventually need trimming or summarization to stay under context limits, especially if you're also sending system prompts or tool definitions.
Handling errors and reconnects mid-stream
Real-time chat apps fail in ways plain request/response apps don't: dropped connections mid-stream, rate limits hit partway through generation, or the model stopping early due to a stop_reason like max_tokens. Always check the final event in the stream to see why generation ended, and give users a visible "regenerate" option rather than silently truncating a reply.
Simplifying this with SubToAPI
If you'd rather not manage Anthropic SDK versions, retry logic, or key rotation yourself, SubToAPI turns your existing Claude access into a plain HTTPS API with its own sub_live_ keys. The streaming behavior is the same SSE format shown above — you just point your fetch calls at https://api.subtoapi.app/v1/messages instead of Anthropic directly, which is useful if you're building a product for a team and want per-member keys, usage visibility, and billing in one dashboard rather than sharing a single raw API key across your app. See /docs/streaming for the exact event format and /docs/quickstart to get a key in a few minutes.
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 1024,
"stream": true,
"messages": [{"role": "user", "content": "Hello"}]
}'
This is drop-in compatible with the Messages API shape, so the frontend code above doesn't need to change — only the base URL and auth header.
Putting it together
A production-ready real-time chat app built on Claude typically has:
- A streaming backend endpoint that proxies the Messages API with
stream: true - A frontend that parses SSE events and repaints the current message incrementally
- Client-side conversation history sent on every request
- Stop-reason handling for truncated or error-ended generations
- Rate-limit and retry logic, especially under concurrent users
Start with the code above, test it with a single hardcoded conversation, then wire in your actual UI state management (React, Vue, or plain DOM) once the streaming mechanics work end to end.
questions
Does the Claude API support WebSockets for chat apps? No. Claude's API uses HTTP with Server-Sent Events for streaming responses. You can wrap this in a WebSocket on your own backend if your app architecture requires it, but Anthropic's native streaming protocol is SSE.
Why does my chat UI show the full response at once instead of streaming? Usually because stream: true wasn't set in the request, or your backend is buffering the response fully before sending it to the client instead of forwarding chunks as they arrive.
How do I keep conversation context across multiple messages? The API is stateless, so you must resend the full message history (or a trimmed/summarized version of it) with every request. There's no server-side session concept — your app owns that state.