When I first built the chatbot on my portfolio, it worked… but let’s be honest—it felt a bit stiff.
You’d type a message, wait a few seconds, and then boom—the entire response appears at once. Functional? Yes. Exciting? Not really.
So I decided to upgrade the experience using Server-Sent Events (SSE)—and wow, what a difference it made.

The Original Setup (REST API)
My chatbot originally used a simple POST request:
const response = await axios.post(`${apiUrl}/chat`, {
message: message,
});
setMessages((prev) => [
...prev,
{
text: response.data.message,
role: "bot",
},
]);
The problem?
- The UI waited for the full response.
- There was no feedback while the AI was “thinking”.
- It felt slow, even when it wasn’t.
Instead of waiting for the full response, I switched to streaming the response in chunks. Now, the AI reply appears gradually, just like ChatGPT typing in real-time.
What I Learned
This upgrade taught me a few important lessons:
- Perceived speed matters more than actual speed: Even if the response still takes 5 seconds, streaming makes it feel instant.
- Simplicity beats overengineering: I initially considered WebSockets, but SSE turned out to be easier to implement and perfect for one-way communication (server to client).
- React state management needs to be careful: I ran into bugs (like duplicated text 😅) because I relied on array indices. The fix was to use unique IDs instead.
- Don’t mix streaming with typing libraries: I was using a typing animation library… while streaming text. Result? Chaos😂. Streaming is already the animation.
How It Works Now
🔧 Express (Backend with SSE)
router.get("/chat-stream", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders();
const stream = await cohere.chatStream({
model: "command-a-03-2025",
messages: [
{ role: "user", content: req.query.message },
],
});
for await (const event of stream) {
if (event.type === "content-delta") {
const chunk = event.delta.message.content.text;
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
}
}
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
res.end();
});
⚛️ React (Frontend Streaming)
const handleSubmit = (message) => {
const botId = Date.now();
setMessages((prev) => [
...prev,
{ id: botId, text: "", role: "bot", done: false },
]);
const eventSource = new EventSource(
`${apiUrl}/chat-stream?message=${encodeURIComponent(message)}`
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.chunk) {
setMessages((prev) =>
prev.map((msg) =>
msg.id === botId
? { ...msg, text: msg.text + data.chunk }
: msg
)
);
}
if (data.done) {
eventSource.close();
}
};
};
SSE vs WebSockets vs REST
Here’s a simple comparison based on my experience:
| Feature | REST (Original) | SSE 📡 | WebSockets 🔌 |
|---|---|---|---|
| Real-time updates | ❌ No | ✅ Yes | ✅ Yes |
| Complexity | ✅ Very Low | ✅ Low | ❌ Higher |
| Streaming support | ❌ No | ✅ Yes | ✅ Yes |
| Bidirectional | ❌ No | ❌ No | ✅ Yes |
| Best use case | Simple APIs | AI chat streaming | Live chat apps |
| Setup effort | ✅ Easy | ✅ Easy | ❌ Moderate |
Why I Chose SSE
For my use case (AI chatbot):
- I only needed server/client communication.
- I wanted streaming.
- I wanted something simple and clean.
👉 SSE was the perfect fit. The final Result after the upgrade was that responses feel instant, the chatbot feels alive, and UX is way more engaging.
And honestly… it just looks cool watching the AI type in real time 😄.

Final Thoughts
If you’re building an AI chatbot, a streaming UI, or anything with progressive responses 👉 try SSE before jumping to WebSockets. It might be all you need. If you’ve built something similar or are thinking about upgrading your app, I’d love to hear about it! Also, make sure to say hi to my AI Chatbot by clicking on the button that has a robot icon in the bottom left corner of this page.
