Server-Sent Events
Real-Time Notifications in NestJS: A Simpler Alternative to WebSockets with Server-Sent Events
Recently, I was searching for an efficient way to notify users of my app. Initially, I considered using AJAX requests in a loop to fetch data from the server, but this approach was far from optimal. Another option was WebSockets, which felt like overkill for simple notifications, such as those seen on social networks or platforms like Medium. Then, I discovered a native and straightforward solution: Server-Sent Events (SSE).
Server-Sent Events
In the traditional web model, a page must request new data from the server. However, with SSE, the server can push new data to the web page at any time. These messages are processed as events and data within the web page, offering a more efficient and seamless notification system.
Server-Sent Events (SSE) provide a mechanism for servers to push updates to clients over a single, long-lived HTTP connection. This is particularly useful for applications that require real-time updates, such as notifications, live feeds, or any scenario where the server needs to send updates to the client without the client having to request them.
Key Features:
1. Simplicity: SSE operates over standard HTTP, making it easier to implement and manage compared to WebSockets. It uses a unidirectional communication model where data flows from the server to the client.
2. MIME Type: SSE uses the text/event-stream MIME type for its responses. This tells the client that the connection will be used to stream events.
3. Automatic Reconnection: The browser automatically handles reconnections if the connection is lost, without requiring additional logic from the server or client.
4. Event Handling: SSE allows the server to send different types of events. Each event can have an optional event name, which the client can listen for using JavaScript event listeners.
5. Data Format: Each message sent from the server can include several fields:
data: The main content of the message.
id: An optional identifier for the message, which can be used to resume the connection from the last event received.
event: An optional event type that the client can listen for.
retry: An optional reconnection time in milliseconds.
Advantages of SSE:
Ease of Use: SSE is simpler to implement than WebSockets, especially for unidirectional data flow.
Built-in Reconnection: Automatic reconnection and event ID tracking make it robust for real-time applications.
Compatibility: SSE is supported by most modern browsers.
Limitations:
Unidirectional: SSE only supports server-to-client communication. If bidirectional communication is needed, WebSockets might be more appropriate.
Limited Browser Support: While most modern browsers support SSE, some older versions may not.
Implementing SSE in the Nest.js APP
NestJS has built-in support for Server-Sent Events (SSE), making their implementation straightforward. To enable SSE on a route within a controller, you can annotate the method handler with the @Sse() decorator. This decorator simplifies the process of setting up a route to stream events to the client.
Example:
@Sse('sse')
sse(): Observable<MessageEvent> {
return interval(1000).pipe(map((_) => ({ data: { hello: 'world' } })));
}The method must return an Observable of MessageEvent objects. An EventSource instance establishes a persistent connection to an HTTP server, which transmits events using the text/event-stream format. This connection remains open until explicitly closed by invoking EventSource.close().
Frontend Implementation
The EventSource object provides all the necessary features for connecting to a server, receiving events and data, handling errors, and closing the connection. Here’s a simple example:
const eventSource = new EventSource('/sse');
eventSource.onmessage = ({ data }) => {
console.log('New message', JSON.parse(data));
};
// handling errors
eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
// Optionally, implement logic to handle reconnection attempts
};Implementing SSE requires just eight lines of code on both the backend and frontend. That’s all it takes!
Browser compatibility
SSE is supported by most modern browsers, but you can check detailed compatibility on caniuse.com.
Example Application
For a practical demonstration, you can find the source code here: GitHub Repository.
In this simple implementation, I simulate sending and receiving notifications.
Test it here: https://nestjs-server-sent-events-511589285765.europe-central2.run.app/
Frontend
The example code includes a basic web page to test the behavior. Launch the app following the Readme instructions and open http://localhost:3000/?user=1 and http://localhost:3000/?user=2 in two different tabs or browsers. The user parameter simulates the authenticated user.
Using the provided form, you can send notifications by selecting the target user and entering the content. Once a notification is sent and processed by the client, it will be pushed to the tab listening to SSE for that user, updating the badge in the header accordingly.
const eventSource = new EventSource(`http://localhost:3000/api/notifications/${userId}/stream`);
// Event listener for new messages
eventSource.onmessage = ({data}) => {
console.log('New message', JSON.parse(data));
const notificationCount = document.getElementById('notifcationCount');
// update badge
notificationCount.textContent = parseInt(notificationCount.textContent) + 1;
};Backend
On the backend, I create a new endpoint that accepts a userId parameter. This endpoint listens for events from the application’s EventEmitter. It filters events based on the userId specified in the notification object and then pushes the relevant events to the client.
@Sse('notifications/:id/stream')
notifications(@Param('id') id: string): Observable<MessageEvent> {
// Return an observable that emits notifications for the given user
return fromEvent(this.eventEmitter, EVENT_USER_NOTIFICATION).pipe(
// Filter notifications by user ID
filter((payload: Notification) => payload.userId === id),
// Map the payload to a MessageEvent
map(
(payload) =>
// Create a new MessageEvent with the notification payload
new MessageEvent('message', {
data: JSON.stringify(payload),
} as MessageEventInit),
),
);
}The NotificationService I created simulates a notification system using in-memory storage. When a new notification is added, it triggers an event, which is then handled by the controller.
createNotification(notification: Notification) {
// Add the notification to the in-memory store
this.notifications.push(notification);
// Emit an event to notify the client
this.eventEmitter.emit(EVENT_USER_NOTIFICATION, notification)
}Warning: When not used over HTTP/2, SSE suffers from a limitation to the maximum number of open connections, which can be especially painful when opening multiple tabs, as the limit is per browser and is set to a very low number (6). The issue has been marked as “Won’t fix” in Chrome and Firefox. This limit is per browser + domain, which means that you can open 6 SSE connections across all of the tabs to
www.example1.comand another 6 SSE connections towww.example2.com(per StackOverflow). When using HTTP/2, the maximum number of simultaneous HTTP streams is negotiated between the server and the client (defaults to 100).
Conclusion
To my surprise, Server-Sent Events (SSE) is not a widely known technique for efficiently pushing data from the server to the client. SSE offers a simpler alternative to WebSockets, reducing complexity while effectively handling real-time data updates.
Source code:
GitHub - peterkracik/nestjs-server-sent-events: Example project of using Server-Sent Events with…
Example project of using Server-Sent Events with Nest.js - peterkracik/nestjs-server-sent-eventsgithub.com
Demo: https://nestjs-server-sent-events-511589285765.europe-central2.run.app/
web: https://kracik.sk
X: https://x.com/peterkracik
LinkedIn: https://www.linkedin.com/in/peterkracik/
sources:
NestJS docs — Server-Sent Events
Developer Mozilla — Server-sent events
Developer Mozilla — Using server-sent events
HTML Standards — Server-sent events
In Plain English 🚀
Thank you for being a part of the In Plain English community! Before you go:
Be sure to clap and follow the writer ️👏️️
Follow us: X | LinkedIn | YouTube | Discord | Newsletter | Podcast
More content at PlainEnglish.io



