Implement real time notifications between NestJs and Dart.

Note: This article is the same I wrote on Medium here.

SSE (SERVER-SENT EVENTS)

To implements real time notifications in a Client-Server architecture, we have a lot of options like web-sockets, pooling, or message queues. But in this article, we will be using an HTTP based pushing technology called SSE (Server-Sent Events). To be short and concise, before technology like SSE, client side application were able to get notified by the server, but they had to check if there was any notifications/updates available on the server side. That was working but with so much complexity and resources waste. With SSE, the client application get notified automatically by the server once the connection between them as been established. This is very similar to web-sockets, the main difference here is that web-sockets are full duplex and bidirectional meanwhile SSE are unidirectional.
You might be thinking that web-sockets are better than SSE, you’re probably right but not totally since in software engineering, we have to choose the right technology depending on the use case. SSE is generally used when trying to send notification from the server to the client without expecting any response from the client. (New authentication pushed notification, the product availability been decremented on the UI when an order is emitted for that product, etc…)

HOW DOES SSE WORKS?

The first is the one where the client app open a long-lived HTTP connection between it and the server app. The server periodically sends data in the form of text-based events to the client, which listens for updates. In the browser, you use the EventSource API to establish a connection and define event handlers. This allows the server to push updates or notifications to the client without the need for frequent polling. SSE is lightweight, easy to implement, and well-suited for scenarios where real-time updates are required, such as live feeds, notifications, or dashboard updates.

IMPLEMENTATION

In this section, I will show you how to implement real time notification in you system. Notifications are sent from the NestJs app to all the listening clients — Flutter.

Requirements: A NestJs app, a Dart project

1. Install packages in the NestJs app:

  • @nestjs/event-emitter : For events emission. In NestJs events have name (Token). We use these tokens to perform actions when the corresponding event is emitted in the same app.
  • rxjs : A JavaScript library for reactive programming that we will use to easily manage event emission, We will not explore this amazing packages in this article here but I strongly recommend you to use it inside your projects.

To be able to use the @nestjs/event-emitter package in your app, you need to imports it. Easily, you can do it by adding it to the imports of the supposed default app.module.ts file as follow:

import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
  imports: [
    EventEmitterModule.forRoot({
      global: true
    }),

  ],
})

export class AppModule {}

2. Trigger notification pushing:

When an event occurs in your app, for example a new order, you want to notify the customer about that. This is how you can do it in NestJs:

import { Injectable } from "@nestjs/common";
import { EventEmitter2 } from "@nestjs/event-emitter";

@Injectable()
export class OrdersService {

    constructor(private readonly eventEmitter: EventEmitter2) { }

    async createOrder(): Promise<Order> {
        let order: Order;
        ...
        order = ...;
        this.eventEmitter.emit('new.order', { order })
        return order
    }
}

Note that here, new.order is the token/key/name of that particular event. The event emitter is responsible for dispatching the event through the server app.

3. Push notifications

In NestJS, to enable Server-Sent events on a route (route registered within a controller class), you need to annotate the method handler with the @Sse() decorator like this:

import { Controller, Sse } from "@nestjs/common";
import { EventEmitter2 } from "@nestjs/event-emitter";
import { fromEvent, map } from "rxjs";

@Controller('orders')
export class OrdersController {
    
    constructor(private readonly eventEmitter: EventEmitter2){}

    @Sse()
    sseOrders(): Promise<MessageEvent<Order>> {
        return fromEvent(this.eventEmitter, 'new.order').pipe(
            map((data) => {
                const order = data['order'] as Order
                return {data: order} as MessageEvent<Order>
            })
        )
    }
}

What’s happening here is very simple. Since, the @Sse decorator must returns an Observable, we are listening to the event new.order with the rxjs method fromEvent and convert the result into an observable using pipe. We are using a signature of that method that the NodeCompatibleEventEmitter and the name of the event as arguments. The method returns an object from which we get the order emitted from the previous step. That order is returned in a MessageEvent object. This is what the client will receive as data.

4. Receive the data (Dart)

To receive data on the client side, we use the so popular package called http. So make sure you have it installed on the client app: dart pub add http. It will helps you to parse the incoming data (text/event-stream) and convert them into readable string.

static final client = http.Client();

final req = http.Request(
        "GET",
        Uri.parse("SERVER_URL/orders/sse")
);

req.headers.addAll({
  HttpHeaders.acceptHeader: "application/json",
  HttpHeaders.contentTypeHeader: "application/json"
});

void open() async {
  final res = await client.send(req);
  res.stream.toStringStream().listen((event) {}).onData(
        (dataString) {
           if(dataString.trim().isEmpty) return;
           log(dataString);
        }
  );
}

Here, we are opening a one time connection between the client app and the server app. While this connection is opened, each time an order is will be created on the server side, an event will be push inside the stream with the containing data and the client side will get notified. Since the dataWhen the server has new data to send, it sends an HTTP response with a special MIME type “text/event-stream”. arrive in String, you’ll have to decode it and make all the required serialization by your own. You can use the built-in jsonDecode method to perform the decoding from String to Map.

CONCLUSION

Using NestJs and Server-Sent Events can be a great choices when you need to handle real time data inside your applications. It doesn’t have to be complicated to be good and cheap. My name is Brice Kamhoua, Software Engineer specialized in Backend and Mobile development with Flutter. Thanks for reading, you can follow me on X(Twitter): kamsbrice.

Subscribe to my newsletter

One update per week. All the latest posts directly in your inbox.