real time updates SSE Mongo

Real-time Updates with Server-Sent Events (SSE) in Next.js and MongoDB


Monday, July 31, 2023
By Simon Kadota
Share

In today’s fast-paced world of web development, real-time updates have become a crucial requirement for modern applications. Whether it’s live chat applications, real-time notifications, or collaborative platforms, users expect their data to be up-to-date at all times. Server-Sent Events (SSE) provide an elegant and efficient solution to achieve real-time communication between the server and the client.

In this step-by-step guide, we will explore how to implement Server-Sent Events (SSE) in a cutting-edge Next.js application. Leveraging the latest features of Next.js, TypeScript, Tailwind CSS, and the experimental app direction, we will build a robust real-time update system that seamlessly synchronizes data between the backend and frontend.

Follow along with the full repo of this tutorial example project here

Why Choose Server-Sent Events (SSE)?

Before diving into the implementation details, let’s briefly understand why SSE is an excellent choice for real-time updates in Next.js applications.

  1. Efficiency and Simplicity: SSE is built on top of HTTP, making it easy to integrate into existing web applications. It enables efficient data streaming from the server to the client without the overhead of establishing multiple connections.
  2. Real-time Communication: SSE allows the server to push data updates to the client in real-time, eliminating the need for constant client-side polling. This results in faster and more responsive applications.
  3. Browser Support: SSE is natively supported by modern browsers, making it a reliable choice for implementing real-time features without the need for additional third-party libraries.
  4. Single Connection: SSE maintains a single persistent connection between the server and the client, reducing the overall network traffic and server load.

Technologies Used

For this tutorial, we will be using the following technologies:

  • Next.js: The latest version of Next.js with the experimental app direction provides an optimized framework for building modern web applications, including serverless deployment and API routes.
  • TypeScript: TypeScript adds static typing to JavaScript, enhancing code quality, and providing a smoother development experience.
  • Tailwind CSS: Tailwind CSS offers a utility-first approach to styling, enabling rapid UI development and easy customization.
  • MongoDB: MongoDB is a popular NoSQL database that we will use to store our data and demonstrate real-time updates with Server-Sent Events (SSE).

Prerequisites

To follow along with this tutorial, you should have a basic understanding of JavaScript, Node.js, and npm (Node Package Manager). Additionally, we assume you have a MongoDB instance set up in replica mode for the real-time updates.

Now that we are equipped with the essential knowledge, let’s dive into the exciting world of real-time updates with Server-Sent Events (SSE) in Next.js!

Step-by-Step Setup

Now that we have the prerequisites covered, let’s proceed with the setup process:

1. Create a New Next.js Project

To create a new Next.js project, we’ll use the create-next-app command with some additional options for TypeScript and Tailwind CSS integration:

npx create-next-app@latest next-sse-mongo --ts --tailwind –app

The above command creates a new Next.js project named `next-sse-mongo` with TypeScript and Tailwind CSS support.

2. Install Necessary Dependencies

Next, navigate to the project directory and install the required npm packages for MongoDB:

cd next-sse-mongo
npm install mongoose

We install `mongoose` to interact with MongoDB in our Next.js app

3. Creating the User Model

Now, let’s create the User model that we’ll use in our change stream setup. In your project, create a new file `model/User.ts` and add the following code:

models/User.ts

import mongoose, { Document, Model } from 'mongoose';
const { Schema, model } = mongoose;
export interface UserDoc extends Document {
  fname?: string;
  lname?: string;
  username?: string;
  // Add other properties as needed
}
const UserSchema = new Schema<UserDoc>({
  fname: String,
  lname: String,
  username: String,
  // You can add more fields specific to the User model here
});
const User: Model<UserDoc> = mongoose.models.User || model<UserDoc>('User', UserSchema);
export default User;

In the code above, we define the `User` model using the Mongoose library, representing a MongoDB collection named `users`. Customize the schema and properties as needed for your application.

The MongoDB records in the `User` collection should look like this:

With this, we have completed the initial setup of our Next.js app. In the upcoming sections, we will implement the Server-Sent Events (SSE) functionality to achieve real-time updates using the `User` model we just created.

Section 2: Creating the MongoDB Change Stream

In this section, we will explore the concept of MongoDB Change Streams and their crucial role in implementing Server-Sent Events (SSE) to achieve real-time updates in our Next.js app. We’ll set up the MongoDB Change Stream and handle change events to listen for real-time changes in the `User` collection.

MongoDB Change Streams and SSE

MongoDB Change Streams provide a real-time stream of changes made to a MongoDB collection. They allow applications to react to database changes in real-time, making them an ideal choice for implementing SSE. By listening to these change events, we can instantly push updates from the server to connected clients, enabling real-time data updates without the need for manual polling.

Setting Up the MongoDB Change Stream

    Create a new file named `mongoChangeStream.ts` in the lib directory of your Next.js project.

    In this file, import the required dependencies and the User model that we created earlier:

lib/mongoChangeStream.ts

import mongoose from 'mongoose';
import User from '@/models/User';

    Connect to the MongoDB database using the connection URI provided in your environment configuration (`.env` file or environment variables in `next.config.js`):

const uri = process.env.MONGO_URI || '';
mongoose.connect(uri);

    Now, we’ll create the MongoDB Change Stream for the User collection:

console.log('Setting up change stream');
const changeStream = User.watch();

    Next, we’ll listen for change events using the change event and handle the changes accordingly:

changeStream.on('change', change => {
  console.log('Change: ', change);
});
changeStream.on('error', error => {
  console.log('Error: ', error);
});

Handling Change Events

In the code above, the `changeStream` object listens for changes in the `User` collection. When a change occurs, the `’change’` event is triggered, and the corresponding change data is logged to the console. Additionally, we listen for the `’error’` event to handle any potential errors that may occur during the change stream process.

With this, we have successfully set up the MongoDB Change Stream to watch for real-time changes in the `User` collection. The next step is to implement the SSE functionality in our Next.js app to establish a connection with the client and push real-time updates using the change stream.

Section 3: Server-Sent Events (SSE) in Next.js

In this section, we will delve into the concept of Server-Sent Events (SSE) and how they enable real-time communication between the server and the client using regular HTTP connections. We’ll implement SSE in Next.js by creating an API route (`/api/sse-working`) to handle SSE connections and push real-time updates to the connected clients.

Understanding Server-Sent Events (SSE)

Server-Sent Events (SSE) is a simple and efficient way to establish a persistent connection between the server and the client. Unlike WebSockets, SSE relies on regular HTTP connections, making it easier to implement and suitable for scenarios where one-way communication (from server to client) suffices. SSE is especially useful for real-time updates, notifications, and streaming data from the server to multiple clients.

Implementing SSE in Next.js

1. Create a new file named `sse.ts` inside the `pages/api` directory of your Next.js app.

2. Import the required dependencies and the `changeStream` from the `mongoChangeStream.ts` file:

pages/api/sse.ts

import { NextApiRequest, NextApiResponse } from 'next';
import { changeStream } from '@/lib/mongoChangeStream'

3. Set up the API route for handling SSE connections:

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // Check if the client accepts SSE
  if (req.headers.accept && req.headers.accept === 'text/event-stream') {
    // Set SSE headers
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

4. Implement the heartbeat mechanism to keep the connection alive:

const HEARTBEAT_INTERVAL = 5000; // 5 seconds (adjust this as needed)
    const intervalId = setInterval(() => {
      // Send a heartbeat message to keep the connection alive
      res.write(': heartbeat\n\n');
    }, HEARTBEAT_INTERVAL);

5. Define a function to send real-time updates to the client:

const sendUpdate = (data: { [key: string]: string }) => {
      const event = `data: ${JSON.stringify(data)}\n\n`;
      res.write(event);
    };

6. Listen for MongoDB change events using the `changeStream` and send updates to the connected clients:

changeStream.on('change', change => {
      // Notify the client about the change
      sendUpdate(change);
    });

7. Handle client disconnect to clean up resources and stop sending updates:

req.socket.on('close', () => {
      // Clean up resources and stop sending updates when the client disconnects
      clearInterval(intervalId);
      res.end();
    });

 

8. If the client does not accept SSE, return a 404 response:

} else {
    // Return a 404 response for non-SSE requests
    res.status(404).end();
  }
}

Summary

In this section, we learned about Server-Sent Events (SSE) and how they enable real-time communication between the server and the client using regular HTTP connections. We implemented SSE in Next.js by creating the `sse.ts` API route to handle SSE connections. The route establishes a persistent connection with the client and pushes real-time updates using MongoDB change events. In the next section, we will focus on implementing SSE on the client-side and updating the UI in response to real-time updates from the server. Let’s move on to the next section to continue building our real-time app with SSE! If you have any questions or need further assistance, feel free to ask. Let’s keep building together!

No worries! Fetching data from your collection is an essential step before we proceed to the front-end implementation of SSE. Let’s add a section to fetch the data from your MongoDB collection using an API route in Next.js.

Section 4: Fetching Data from MongoDB Collection

In this section, we’ll create an API route to fetch data from your MongoDB collection and send it as a response to the client. This data will be used to initialize the UI and display the current status of the devices.

1. Create a new file named `lockDevices.ts` inside the `pages/api` directory of your Next.js app.

2. Import the required dependencies and the `Device` model from the `model/Device.ts` file (if you haven’t created it yet, you can refer to the code snippet provided earlier for creating the `Device` model):

pages/api/lockDevices.ts

import { NextApiRequest, NextApiResponse } from 'next';
import Device from '@/models/Device';

3. Implement the API route to fetch lock devices data:

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    if (req.method === 'GET') {
      // Fetch lock devices data from the database
      const lockDevices = await Device.find({ type: 'lock' });
      // Send the lock devices data as the API response
      res.status(200).json(lockDevices);
    } else {
      res.status(405).json({ message: 'Method not allowed' });
    }
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
}

4. Save the file and ensure that the MongoDB connection is established before querying the database. You can check the MongoDB connection setup in the `mongoChangeStream.ts` file.

Now, with the `lockDevices.ts` API route, you can fetch the data from your MongoDB collection by making a GET request to `/api/lockDevices`. This data will be used to initialize the UI in the front-end section.

In the next section, we’ll move on to implementing the front-end part of our real-time app with SSE. We’ll use the fetched data to display the current status of the devices and update the UI in real-time using SSE. Let’s continue building the real-time app! If you have any questions or need further assistance, feel free to ask. Let’s proceed to the next section together!

Section 5: Frontend Implementation

In this section, we’ll create a new component called `UserCard` in Next.js to display real-time updates of user data. We’ll use React hooks to manage state and establish the SSE connection in the component. Additionally, we’ll fetch initial data from the MongoDB collection and display it in the component. Finally, we’ll demonstrate how to handle SSE messages and update the UI with received data.

Step 1: Create the `UserCard` Component

1. Create a new file named `UserCard.tsx` inside the `components` directory of your Next.js app.

2. Import the necessary dependencies:

components/UserCard.tsx

import { useCallback, useEffect, useState } from 'react';
```
3. Implement the `UserCard` component:
```typescript
export default function UserCard() {
  const [users, setUsers] = useState([]);
  // SSE connection reference
  const [sseConnection, setSSEConnection] = useState<EventSource | null>(null);
  const fetchUsers = async () => {
    try {
      const usersResult = await fetch(`/api/getData`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const users = await usersResult.json();
      setUsers(users);
    } catch (e) {
      console.error(e);
    }
  };
  const listenToSSEUpdates = useCallback(() => {
    console.log('listenToSSEUpdates func');
    const eventSource = new EventSource('/api/sse');
    eventSource.onopen = () => {
      console.log('SSE connection opened.');
      // Save the SSE connection reference in the state
    };
    eventSource.onmessage = (event) => {
      const data = event.data;
      console.log('Received SSE Update:', data);
      fetchUsers();
      // Update your UI or do other processing with the received data
    };
    eventSource.onerror = (event) => {
      console.error('SSE Error:', event);
      // Handle the SSE error here
    };
    setSSEConnection(eventSource);
    return eventSource;
  }, []);
  useEffect(() => {
    fetchUsers();
    listenToSSEUpdates();
    return () => {
      if (sseConnection) {
        sseConnection.close();
      }
    };
  }, [listenToSSEUpdates]);
  // Add "beforeunload" event listener to close SSE connection when navigating away
  useEffect(() => {
    const handleBeforeUnload = () => {
      console.dir(sseConnection);
      if (sseConnection) {
        console.info('Closing SSE connection before unloading the page.');
        sseConnection.close();
      }
    };
    window.addEventListener('beforeunload', handleBeforeUnload);
    // Clean up the event listener when the component is unmounted
    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, [sseConnection]);
  useEffect(() => {
    console.log('users', users);
  }, [users]);
  return (
    <div className="m-4 p-2 rounded-lg shadow-sm flex flex-col bg-gray-50">
      {users.map((user, index) => {
        return (
          <div key={index} className="my-2 flex text-slate-800">
            {`${user.fname} ${user.lname} - ${user.username}`}
          </div>
        );
      })}
    </div>
  );
}

Step 2: Render the `UserCard` Component

1. In your desired page (e.g., `index.tsx`), import and render the `UserCard` component:

import UserCard from '@/components/UserCard';
export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <UserCard />
    </main>
  );
}

With the updated `UserCard` component and `index.tsx` page, we’ve successfully implemented the frontend part of our real-time app with SSE. The `UserCard` component fetches initial user data from the MongoDB collection and displays it. The SSE connection is established, and the component updates the UI in real-time with received data. Now, you have a functional real-time app using Server-Sent Events in Next.js! If you have any questions or need further assistance, feel free to ask. Happy coding!

Section 6: Handling SSE Errors and Disconnections

In this section, we’ll address potential issues with SSE connections and provide solutions for handling errors and client disconnections. We’ll discuss common problems that might occur, such as errors during page transitions and clients unexpectedly disconnecting. Additionally, we’ll show code examples for handling SSE errors and implementing fallback mechanisms to handle failed connections.

Step 1: Handling SSE Errors

When working with SSE, it’s essential to be aware of possible errors that might occur during the connection lifecycle. Errors can happen due to network issues, server-side problems, or client-side issues. To handle SSE errors effectively, you can use the `onerror` event to listen for any errors that occur during the SSE connection.

Example:

eventSource.onerror = (event) => {
  console.error('SSE Error:', event);
  // Handle the SSE error here, such as closing the connection or displaying an error message to the user.
};

By logging or handling the SSE error appropriately, you can troubleshoot and debug issues related to the connection and provide a better user experience by informing the user about the problem.

Step 2: Closing SSE Connection on Unmount

One issue you may encounter is when switching pages or reloading the page with the SSE component on it. In such cases, it’s crucial to close the SSE connection properly to avoid resource leaks and unexpected behavior. To achieve this, you can use the `beforeunload` event to listen for the page navigation event and close the SSE connection before the page unloads.

Example:

useEffect(() => {
  const handleBeforeUnload = () => {
    if (sseConnection) {
      console.info('Closing SSE connection before unloading the page.');
      sseConnection.close();
    }
  };
  window.addEventListener('beforeunload', handleBeforeUnload);
  // Clean up the event listener when the component is unmounted
  return () => {
    window.removeEventListener('beforeunload', handleBeforeUnload);
  };
}, [sseConnection]);

By implementing this cleanup logic, you ensure that the SSE connection is closed correctly when the user navigates away from the SSE page or when the component is unmounted.

Step 3: Heartbeat Response to Keep the Connection Alive

Another issue you might encounter with SSE is the connection unexpectedly closing if the server does not provide a response within a certain timeout period. To prevent this, you can send periodic heartbeat responses from the server to keep the connection alive.

Example (Backend – Sending Heartbeat Response):

const HEARTBEAT_INTERVAL = 5000; // 5 seconds (adjust this as needed)
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // Set SSE headers
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  const intervalId = setInterval(() => {
    // Send a heartbeat message to keep the connection alive
    res.write(': heartbeat\n\n');
  }, HEARTBEAT_INTERVAL);
  // ... Rest of the SSE implementation
  // Handle client disconnect
  req.socket.on('close', () => {
    // Clean up resources and stop sending updates when the client disconnects
    clearInterval(intervalId);
    res.end();
  });
}

By regularly sending heartbeat responses from the server, you maintain the SSE connection open and prevent it from unexpectedly closing due to inactivity.

With the SSE error handling, proper connection closing on unmount, and the heartbeat response mechanism, you can enhance the stability and reliability of your SSE implementation in Next.js. This ensures that your real-time app provides a smooth and uninterrupted user experience even in the face of potential network issues or client-side changes.

Conclusion:

In this blog post, we explored the exciting world of Server-Sent Events (SSE) and how they enable real-time updates in web applications. We began by understanding the basics of SSE and its role in establishing a persistent connection between the server and the client. SSE offers a lightweight and efficient way to send real-time data from the server to the client without the need for continuous polling.

To implement SSE in a Next.js application, we followed a step-by-step approach, covering both the backend and frontend aspects of the process. We set up a MongoDB Change Stream to listen for changes in the database and created an API route to handle SSE connections in Next.js. On the frontend, we built a React component to manage state and established the SSE connection, receiving real-time updates from the server.

By leveraging SSE, we achieved the following benefits:

  1. Real-Time Updates: SSE enabled us to instantly update the user interface with the latest data without the need for manual refreshing or polling.
  2. Reduced Server Load: SSE’s one-way communication approach significantly reduces server load compared to traditional bidirectional approaches like WebSockets.
  3. Improved User Experience: Real-time updates provide a more dynamic and engaging user experience, making applications feel more responsive and interactive.

We hope this blog post has inspired you to implement SSE in your own projects. Whether you’re building a chat application, live dashboard, or real-time monitoring system, SSE can be a powerful tool to enhance your application’s capabilities.

Feel free to explore the code examples provided in this blog post and adapt them to suit your specific use case. If you have any questions or face challenges during the implementation, don’t hesitate to reach out to our web development team and ask for assistance. We love to hear your feedback and experiences!