Say you have an application that needs to display real-time data to the user. For example, a dashboard that displays the number of users currently online, or a chat application that displays new messages as they arrive, or a stock market application that displays the latest stock prices.
This is a common problem that many applications face, and usually, the first solution that comes to mind is to use WebSockets. However, WebSockets are not the only solution to this problem. In fact, there is a much simpler solution that is often overlooked or unknown to many developers: Server-Sent Events (SSE).
In this post, I'll walk you through setting up Server-Sent Events (SSE) in Astro to build a lightweight real-time chat server.
What are Server-Sent Events (SSE)?
Server-Sent Events (SSE) is a web standard enabling servers to push data to clients over a single, long-lived HTTP connection. It's a perfect fit for unidirectional data flows where the server needs to update the client, such as sending JSON payloads, text updates, or HTML fragments to dynamically refresh the UI.
One of the key advantages of SSE is its native support in modern browsers, eliminating the need for external libraries or polyfills. It's also generally easier on server resources compared to WebSockets, especially when bidirectional communication isn't required.
Setting Up Astro
Before diving into the chat functionality, you'll need an Astro project. If you're starting from scratch, use the following command to create a fresh Astro project and add the Node.js adapter.
Creating the Chat Logic
First, we need to create a basic chat controller that will handle the chat logic on the server. This is the heart of our application. It will store messages in memory and will provide a way for us to subscribe to new messages as they arrive.
Let's create a file called chat.ts in the src/controllers directory and let's get to work.
Next, I'm going to create a messages array to store the messages in memory with a getMessages() method to retrieve the messages and an addMessage() method to add new messages to the array.
Finally, I'm going to create a subscribe() and unsubscribe() method to allow us to subscribe to new messages as they arrive. For this, I'm going to use the EventEmitter class from Node.js.
As you can see, I'm using the on() method to subscribe to the message event and the off() method to unsubscribe from the message event. I'm also using the emit() method to emit the message event when a new message is added to the messages array.
Creating the Chat Page
Now that we have the chat logic in place, let's create a page to display the chat messages.
Here, we are server-side rendering the messages using the getMessages() method from the ChatController class. We are also adding a form to allow us to send new messages to the server.
Handle Form Submission
In the same file, let's add some JavaScript to handle the form submission.
This code here is pretty straightforward. We are adding an event listener to the submit event of the form and sending a POST request to the /pages/api/chat endpoint with the message as the body of the request. We are also clearing the input field after the request is sent to make it a bit more user-friendly.
Note: I am using the is:inline attribute to inline the JavaScript code in the HTML file.
Creating the Chat API
However, this code here doesn't do anything yet. We need to create an endpoint to handle the POST request and add the message to the messages array.
Here we are creating a REST API endpoint that will handle the POST request and add the message to the messages array. We are also returning a 204 status code to indicate that the request was successful.
With this in place, we can now send messages to the server. However, if you try this out, you'll notice that new messages are not being reflected in the UI. This is because we are not updating the UI when new messages are added to the messages array.
This is where SSE comes in. Let's see how we can use SSE to stream the messages to the browser in real-time.
Setting Up SSE Endpoint
To set up SSE, we need to create a new endpoint that will handle the GET request and send the messages to the client as they arrive.
Let's break this down. First, we are creating a new ReadableStream and passing it to the Response constructor. This will allow us to send data to the client as it arrives.
ReadableStream takes a start() function with a controller argument. This controller argument allows us to enqueue data to the stream using the enqueue() method. This is how we will send data to the client.
Next, we are creating a sendEvent() function that will take the data as an argument and send it to the client.
SSE requires us to send the data in a specific format. Each message must be prefixed with data:  and suffixed with \n\n. This is why we are using the JSON.stringify() method to convert the data to a string and then prefixing it with data:  and suffixed with \n\n. We are then using the TextEncoder class to convert the string to a Uint8Array before sending it to the client.
There are other optional fields that we can send to the client, such as id, event, and retry. However, we are not going to use them in this example.
Finally, we are adding an event listener to the request.signal object to handle the connection closing. This will allow us to close the stream when the connection is closed.
Subscribing to New Messages
This code here, however, does not do anything yet. We need to bring in the ChatController class and subscribe to new messages as they arrive.
Here, every time a new connection is made, we are subscribing to new messages using the subscribe() method from the ChatController class. We are also unsubscribing from new messages when the connection is closed using the unsubscribe() method from the ChatController class. This will ensure that we don't send messages to the client when the connection is closed.
Subscribing to SSE
Now that we have the SSE endpoint in place, let's go back to the chat page and subscribe to the SSE endpoint.
Browsers that support SSE (which is most modern browsers) provide a built-in EventSource class that allows us to subscribe to SSE endpoints. We can then use the onmessage event handler to listen for new messages and consume them as they arrive.
In this case, I am using the onmessage event handler to listen for new messages, parse the data, and append it to the list of messages.
Bringing It All Together
With all the components in place, it's time to test our real-time chat application.
Run the development server:
Open the application in your browser, send messages, and watch as they appear in real-time across multiple tabs or windows.
That is cool, isn't it? 😎
Conclusion
That's it! We have successfully set up Server-Sent Events (SSE) in Astro and created a simple chat server that streams messages to the browser in real-time.
The nice thing about SSEs is that they are simple to use and they work over a single, long-lived HTTP connection. This makes them ideal for sending JSON, text data, or even HTML snippets to update the client's UI in real-time.
Additionally, a nice feature of the EventSource class is that it will automatically reconnect if the connection is lost. This means that if the connection is lost for any reason, the client will automatically reconnect and continue receiving messages.
I hope this post has encouraged you to give it a try and see how it can help you build better applications.
Happy coding, and until next time, keep streaming those events! 🚀
Interested in LogSnag?

