Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Introducing MongoDB 8.0, the fastest MongoDB ever!
MongoDB Developer
JavaScript
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
JavaScriptchevron-right

Real-Time Location Tracking With Change Streams and Socket.io

Ashiq Sultan8 min read • Published Feb 09, 2023 • Updated Aug 13, 2024
Node.jsMongoDBChange StreamsJavaScript
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
In this article, you will learn how to use MongoDB Change Streams and Socket.io to build a real-time location tracking application. To demonstrate this, we will build a local package delivery service.
Change streams are used to detect document updates, such as location and shipment status, and Socket.io is used to broadcast these updates to the connected clients. An Express.js server will run in the background to create and maintain the websockets.
This article will highlight the important pieces of this demo project, but you can find the full code, along with setup instructions, on Github.
preview of location updates in user app when driver simulator is updated

Connect Express to MongoDB Atlas

Connecting Express.js to MongoDB requires the use of the MongoDB driver, which can be installed as an npm package. For this project I have used MongoDB Atlas and utilized the free tier to create a cluster. You can create your own free cluster and generate the connection string from the Atlas dashboard.
I have implemented a singleton pattern for connecting with MongoDB to maintain a single connection across the application.
The code defines a singleton db variable that stores the MongoClient instance after the first successful connection to the MongoDB database.The dbConnect() is an asynchronous function that returns the MongoClient instance. It first checks if the db variable has already been initialized and returns it if it has. Otherwise, it will create a new MongoClient instance and return it. dbConnect function is exported as the default export, allowing other modules to use it.
1// dbClient.ts
2import { MongoClient } from 'mongodb';
3const uri = process.env.MONGODB_CONNECTION_STRING;
4let db: MongoClient;
5const dbConnect = async (): Promise<MongoClient> => {
6    try {
7        if (db) {
8            return db;
9        }
10        console.log('Connecting to MongoDB...');
11        const client = new MongoClient(uri);
12        await client.connect();
13        db = client;
14        console.log('Connected to db');
15        return db;
16    } catch (error) {
17        console.error('Error connecting to MongoDB', error);
18        throw error;
19    }
20};
21export default dbConnect;
Now we can call the dbConnect function in the server.ts file or any other file that serves as the entry point for your application.
1// server.ts
2import dbClient from './dbClient';
3server.listen(5000, async () => {
4    try {
5        await dbClient();
6    } catch (error) {
7        console.error(error);
8    }
9});
We now have our Express server connected to MongoDB. With the basic setup in place, we can proceed to incorporating change streams and socket.io into our application.

Change Streams

MongoDB Change Streams is a powerful feature that allows you to listen for changes in your MongoDB collections in real-time. Change streams provide a change notification-like mechanism that allows you to be notified of any changes to your data as they happen.
To use change streams, you need to use the watch() function from the MongoDB driver. Here is a simple example of how you would use change streams on a collection.
1const changeStream = collection.watch()
2changeStream.on('change', (event) => {
3// your logic
4})
The callback function will run every time a document gets added, deleted, or updated in the watched collection.

Socket.IO and Socket.IO rooms

Socket.IO is a popular JavaScript library. It enables real-time communication between the server and client, making it ideal for applications that require live updates and data streaming. In our application, it is used to broadcast location and shipment status updates to the connected clients in real-time.
One of the key features of Socket.IO is the ability to create "rooms." Rooms are a way to segment connections and allow you to broadcast messages to specific groups of clients. In our application, rooms are used to ensure that location and shipment status updates are only broadcasted to the clients that are tracking that specific package or driver.
The code to include Socket.IO and its handlers can be found inside the files src/server.ts and src/socketHandler.ts
We are defining all the Socket.IO events inside the socketHandler.ts file so the socket-related code is separated from the rest of the application. Below is an example to implement the basic connect and disconnect Socket.IO events in Node.js.
1// socketHandler.ts
2const socketHandler = (io: Server) => {
3  io.on('connection', (socket: any) => {
4    console.log('A user connected');
5    socket.on('disconnect', () => {
6      console.log('A user disconnected');
7    });
8  });
9};
10export default socketHandler;
We can now integrate the socketHandler function into our server.ts file (the starting point of our application) by importing it and calling it once the server begins listening.
1// server.ts
2import app from './app'; // Express app
3import http from 'http';
4import { Server } from 'socket.io';
5const server = http.createServer(app);
6const io = new Server(server);
7server.listen(5000, async () => {
8  try {
9    socketHandler(io);
10  } catch (error) {
11    console.error(error);
12  }
13});
We now have the Socket.IO setup with our Express app. In the next section, we will see how location data gets stored and updated.

Storing location data

MongoDB has built-in support for storing location data as GeoJSON, which allows for efficient querying and indexing of spatial data. In our application, the driver's location is stored in MongoDB as a GeoJSON point.
To simulate the driver movement, in the front end, there's an option to log in as driver and move the driver marker across the map, simulating the driver's location. (More on that covered in the front end section.)
When the driver moves, a socket event is triggered which sends the updated location to the server, which is then updated in the database.
1socket.on("UPDATE_DA_LOCATION", async (data) => {
2  const { email, location } = data;
3  await collection.findOneAndUpdate({ email }, { $set: { currentLocation: location } });
4});
The code above handles the "UPDATE_DA_LOCATION" socket event. It takes in the email and location data from the socket message and updates the corresponding driver's current location in the MongoDB database.
So far, we've covered how to set up an Express server and connect it to MongoDB. We also saw how to set up Socket.IO and listen for updates. In the next section, we will cover how to use change streams and emit a socket event from server to front end.

Using change streams to read updates

This is the center point of discussion in this article. When a new delivery is requested from the UI, a shipment entry is created in DB. The shipment will be in pending state until a driver accepts the shipment.
Once the driver accepts the shipment, a socket room is created with the driver id as the room name, and the user who created the shipment is subscribed to that room.
Here's a simple diagram to help you better visualize the flow.
Shipment creation flow diagram
With the user subscribed to the socket room, all we need to do is to listen to the changes in the driver's location. This is where the change stream comes into picture.
We have a change stream in place, which is listening to the Delivery Associate (Driver) collection. Whenever there is an update in the collection, this will be triggered. We will use this callback function to execute our business logic.
Note we are passing an option to the change stream watch function { fullDocument: 'updateLookup' }. It specifies that the complete updated document should be included in the change event, rather than just the delta or the changes made to the document.
1const watcher = async (io: Server) => {\
2  const collection = await DeliveryAssociateCollection();\
3  const changeStream = collection.watch([], { fullDocument: 'updateLookup' });\
4  changeStream.on('change', (event) => {\
5    if (event.operationType === 'update') {\
6        const fullDocument = event.fullDocument;\
7        io.to(String(fullDocument._id)).emit("DA_LOCATION_CHANGED", fullDocument);\
8}});};
In the above code, we are listening to all CRUD operations in the Delivery Associate (Driver) collection and we emit socket events only for update operations. Since the room names are just driver ids, we can get the driver id from the updated document.
This way, we are able to listen to changes in driver location using change streams and send it to the user. 
In the codebase, all the change stream code for the application will be inside the folder src/watchers/. You can specify the watchers wherever you desire but to keep code clean, I'm following this approach. The below code shows how the watcher function is executed in the entry point of the application --- i.e., server.ts file.
1// server.ts
2import deliveryAssociateWatchers from './watchers/deliveryAssociates';
3server.listen(5000, async () => {
4  try {
5 await dbClient();
6 socketHandler(io);
7 await deliveryAssociateWatchers(io);
8  } catch (error) {
9    console.error(error);
10  }
11});
In this section, we saw how change streams are used to monitor updates in the Delivery Associate (Driver) collection. We also saw how the fullDocument option in the watcher function was used to retrieve the complete updated document, which then allowed us to send the updated location data to the subscribed user through sockets. The next section focuses on exploring the front-end codebase and how the emitted data is used to update the map in real time.

Front end

I won't go into much detail on the front end but just to give you an overview, it's built on React and uses Leaflet.js for Map.
I have included the entire front end as a sub app in the GitHub repo under the folder /frontend. The Readme contains the steps on how to install and start the app.
Starting the front end gives two options: 
1. Log in as user.2. Log in as a driver.
Use the "log in as driver" option to simulate the driver's location. This can be done by simply dragging the marker across the map.
frontend welcome screen

Driver simulator

Logging in as driver will let you simulate the driver's location. The code snippet provided demonstrates the use of useStateand useEffect hooks to simulate a driver's location updates. The <MapContainer> and <DraggableMarker> are Leaflet components. One is the actual map we see on the UI and other is, as the name suggests, a marker which is movable using our mouse.
1// Driver Simulator
2const [position, setPosition] = useState(initProps.position);
3const gpsUpdate = (position) => {
4  const data = {
5    email,
6    location: { type: 'Point', coordinates: [position.lng, position.lat] },
7  };
8  socket.emit("UPDATE_DA_LOCATION", data);
9};
10useEffect(() => {
11gpsUpdate(position);
12}, [position]);
13return (
14<MapContainer>
15<DraggableMarker position={position}/>
16</MapContainer>
17)
The position state is initialized with the initial props. When the draggable marker is moved, the position gets updated. This triggers the gpsUpdate function inside its useEffect hook, which sends a socket event to update the driver's location.

User app

On the user app side, when a new shipment is created and a delivery associate is assigned, the SHIPMENT_UPDATED socket event is triggered. In response, the user app emits the SUBSCRIBE_TO_DA event to subscribe to the driver's socket room. (DA is short for Delivery Associate.)
1socket.on('SHIPMENT_UPDATED', (data) => {
2  if (data.deliveryAssociateId) {
3    const deliveryAssociateId = data.deliveryAssociateId;
4    socket.emit('SUBSCRIBE_TO_DA', { deliveryAssociateId });
5  }
6});
Once subscribed, any changes to the driver's location will trigger the DA_LOCATION_CHANGED socket event. The driverPosition state represents the delivery driver's current position. This gets updated every time new data is received from the socket event.
1const [driverPosition, setDriverPosition] = useState(initProps.position);
2socket.on('DA_LOCATION_CHANGED', (data) => {
3  const location = data.location;
4  setDriverPosition(location);
5});
6return (
7<MapContainer>
8<Marker position={driverPosition}/>
9</MapContainer>
10)
The code demonstrates how the user app updates the driver's marker position on the map in real time using socket events. The state driverPosition is passed to the
component and updated with the latest location data from the DA_LOCATION_CHANGED socket event.

Summary

In this article, we saw how MongoDB Change Streams and Socket.IO can be used in a Node.js Express application to develop a real-time system.
We learned about how to monitor a MongoDB collection using the change stream watcher method. We also learned how Socket.IO rooms can be used to segment socket connections for broadcasting updates. We also saw a little front-end code on how props are manipulated with socket events.
If you wish to learn more about Change Streams, check out our tutorial on Change Streams and triggers with Node.js, or the video version of it. For a more in-depth tutorial on how to use Change Streams directly in your React application, you can also check out this tutorial on real-time data in a React JavaScript front end.

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Tutorial

Manage Game User Profiles with MongoDB, Phaser, and JavaScript


Apr 02, 2024 | 11 min read
Code Example

Trends Analyser


Sep 11, 2024 | 1 min read
Tutorial

How to Write Unit Tests for MongoDB Atlas Functions


Sep 09, 2024 | 10 min read
Quickstart

How to Connect MongoDB Atlas to Vercel Using the New Integration


Aug 05, 2024 | 4 min read
Table of Contents