- Published at
Server-Sent Events Explained
Discover the power of Server-Sent Events (SSE) to build dynamic, real-time web applications. Learn how to leverage SSE to create engaging user experiences with live data updates
- Authors
-
-
- Name
- Kumneger Wondimu
- https://x.com/Kumneger0
- Full-stack devleoper at 2f-Capital
-
Table of Contents
what is server sent events
Server-Sent Events (SSE) is a server push technology that allows servers to send real-time updates to web clients over a single, long-lived HTTP connection
SSE vs Web Sockets
Server-Sent Events (SSE) provide one-way communication from server to client, while WebSockets enable two-way, full-duplex communication where both the client and server can send and receive messages
Example Project
Let’s build a small group chat application to demonstrate how to implement Server-Sent Events (SSE). For this example, we will use Hono and Bun (although you may use Node.js if you prefer). All of the code is available on my GitHub repository; feel free to check it out.
To enable Server-Sent Events (SSE) for our application, you can use the streamSSE
function in Hono, which sets the appropriate Content-Type: text/event-stream
header for us. This informs the browser that the response will be an SSE, allowing it to handle the incoming stream of events appropriately.
app.get("/sse", async (c) => {
return streamSSE(c, async (stream) => {
while (true) {
const message = `hello clint`;
await stream.writeSSE({
data: message,
event: "greeting",
});
await stream.sleep(3000);
}
});
});
If you save the code and navigate to your browser at the /sse
endpoint, you will observe that the server sends a new message saying “hello client” every 3 seconds. This is because the server is continuously writing new SSE events with this message at 3-second intervals. This demonstrates the real-time update capability of Server-Sent Events, where the server pushes updates to the client without the client having to request them repeatedly.
What are data and event properties? Data is the information we send to the client, and an event is used to listen for specific events from our client using the EventSource API
The code below handles the /sse
endpoint for a group chat application. Each user must provide a unique id
and a name
. Here’s a breakdown of how it works:
- Parameter Check: When a user connects to the
/sse
endpoint, the server checks for theid
andname
parameters. If either is missing, it sends a message back to the client, requesting bothid
andname
. - Stream Setup: If both
id
andname
are provided, the server initializes a Server-Sent Events (SSE) stream using thestreamSSE
function. This function takes a Hono context and a callback function with a stream object. - Send Current Users: Upon connection, the server sends a list of all currently connected users to the new user.
- Notify Existing Users: The server notifies all existing users about the new user who just joined the chat.
- Handle User Leaving: When a user leaves the chat, the server removes the user from the list and notifies all remaining users that the user has left.
- Periodic Updates: The server continuously sends updates to all connected users every 2 seconds, listing all current users.
This implementation ensures real-time updates and notifications for user connections and disconnections in a group chat using Server-Sent Events (SSE).
const users: { id: string; name: string; stream: SSEStreamingApi }[] = [];
app.get("/sse", async (c) => {
const userID = c.req.query("id");
const name = c.req.query("name");
if (!userID || !name) {
return c.text("id and name are required");
}
return streamSSE(c, async (stream) => {
stream.writeSSE({
event: "all-users",
data: JSON.stringify(users.map((u) => ({ id: u.id, name: u.name }))),
});
users.forEach(({ stream }) => {
stream.writeSSE({
data: JSON.stringify({
id: userID,
name,
}),
event: "new-user",
});
});
users.push({ id: userID, name, stream });
stream.onAbort(() => {
users.splice(
users.findIndex((u) => u.id === userID),
1,
);
users.forEach((user) =>
user.stream.writeSSE({
data: JSON.stringify({ id: userID, name }),
event: "user-leave",
}),
);
console.log(`user ${userID} disconnected`);
});
while (true) {
await stream.writeSSE({
data: users
.map((u) => ({
id: u.id,
name: u.name,
}))
.join("\n"),
event: "users",
id: String(Math.random()),
});
await stream.sleep(2000);
}
});
});
Next, we have the /message
endpoint, which takes a message from the client along with the user’s name
and id
, and distributes it to all currently connected users. Here’s how it works:
- Parse Request Body: When a user sends a message to the
/message
endpoint, the server parses the request body to extract themessage
,id
, andname
. - Broadcast Message: The server then broadcasts the message to all connected users using the
writeSSE
method. - Return Confirmation: Finally, the server responds to the client with the same message content as a JSON object to confirm that the message has been received and processed.
Here’s the implementation:
app.post("/message", async (c) => {
const body = (await c.req.parseBody()) as {
message: string;
id: string;
name: string;
};
users.forEach((user) => {
user.stream.writeSSE({
data: JSON.stringify(body),
event: "message",
});
});
return c.json(body);
});
In this implementation:
- The server first parses the request body to extract the
message
,id
, andname
. - It then loops through all connected users and sends the new message to each of them using the
writeSSE
method. - The
data
field of the SSE contains the message, user ID, and name in JSON format. - The server responds to the client with the same message content as a JSON object, confirming that the message has been successfully sent to all users.
Next, we have the following endpoint to serve static files from the ./public
directory:
app.get(
"*",
serveStatic({
root: "./public",
}),
);
This endpoint handles any incoming GET requests and serves static files from the ./public
directory. For example, if you have an index.html
file in ./public
, it will be served when the client accesses the root URL ("/"
)
Now we are done with the server part.
To handle the client-side functionality, we can use the following implementation:
function getUserName() {
const name = prompt("What is your name?");
if (!name) return getUserName();
return name;
}
const me = {
name: getUserName(),
id: crypto.randomUUID(),
};
handleSSEConnection();
function handleSSEConnection() {
const eventSource = new EventSource(`/sse?id=${me.id}&name=${me.name}`);
eventSource.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
showMessage(data);
});
eventSource.addEventListener("new-user", (event) => {
const user = JSON.parse(event.data);
console.log(user);
showUser(user);
showUserJoinAndLeaveMessage(user, true);
});
eventSource.addEventListener("all-users", (event) => {
const data = JSON.parse(event.data);
showUsers(data);
});
eventSource.addEventListener("user-leave", (event) => {
const data = JSON.parse(event.data);
users = users.filter(({ id }) => data.id !== id);
usersContainer?.childNodes.forEach((node) => node.remove());
showUsers(users);
showUserJoinAndLeaveMessage(data, false);
});
}
In this implementation:
getUserName()
: Prompts the user for their name. If no name is provided, it prompts again until a name is entered.me
: An object representing the current user, containing their name and a unique ID generated usingcrypto.randomUUID()
.handleSSEConnection()
: Establishes a connection to the server using theEventSource
API and listens for various events such asmessage
,new-user
,all-users
, anduser-leave
.
Each event handler processes the received data accordingly:
"message"
: Parses the incoming message data and callsshowMessage(data)
to display it."new-user"
: Parses the data for a new user, logs it, and callsshowUser(user)
andshowUserJoinAndLeaveMessage(user, true)
to display the new user and a join message."all-users"
: Parses the data for all users and callsshowUsers(data)
to display the list of users."user-leave"
: Parses the data for a user who left, removes them from the list, and callsshowUsers(users)
andshowUserJoinAndLeaveMessage(data, false)
to update the user list and display a leave message.
These event handlers—message
, new-user
, user-leave
—are mapped to the event
property from the server side, which is set using the writeSSE
method in the server-side code
Summary
In this article, we demonstrated how Server-Sent Events (SSE) can be used for real-time updates in a group chat application with Hono, Bun, and Vanilla JS. SSE provides a one-way communication channel from the server to the client, ideal for pushing live updates without the complexity of WebSockets.
Key Points:
- Server Setup: We utilized Hono’s
streamSSE
function to manage user connections, broadcast messages, and provide real-time updates. - Client Handling: The
EventSource
API was used to receive and display messages, manage user lists, and handle user join/leave events.
This setup ensures that users receive timely updates and notifications, creating an interactive and responsive chat experience