Building a Location-Aware Backend: A Comprehensive Guide to Implementing Nearby Places Feature with Node.js, and MongoDB

Introduction

Have you ever wondered how applications can provide real-time information tailored to any location of choice? Like, how do they navigate delivery routes, find cool places nearby, or enhance user experiences with location-based features? Well, the answer lies in the power of geospatial data.

What is Geospatial Data?

Geospatial data is essentially information related to a specific location on Earth. For instance, the coordinates of the Eiffel Tower, the population density of Lagos, and the distance between New York and London are all examples of geospatial data. In the realm of developing applications, geospatial features make user experiences more exciting, by providing real-time information tailored to any location of choice.

Geospatial Features in MongoDB

Now, MongoDB is like the superstar of databases, especially when it comes to geospatial things. It's the go-to tool for developers looking to handle location features. MongoDB not only stores and retrieves geospatial data efficiently but does it with speed and intelligence. Armed with cool tools like geospatial indexes and query operators, MongoDB makes incorporating location features a seamless experience. Ready to embark on the geospatial journey with MongoDB? Let's dive into the details together!

Components and Technologies

To bring our straightforward platform to life, we're rolling with these essential components and technologies:

  1. MongoDB: This is our go-to database, storing geospatial data with efficiency. It's not just a storage hub; it lets us play with geospatial indexes and query operators, making location-based searches a breeze.

  2. Express.js: Our simple app is getting dressed up with Express, a sleek Node.js web app framework. It's the guy handling all the chit-chat with our MongoDB database, keeping things smooth and snappy.

  3. Node.js: The engine behind our backend, making server-side JavaScript the star of the show. It's the muscle that brings our platform to life.

Tutorial Project and Objectives

Now that we have introduced the components and technologies, let's dive into the main event: our tutorial project. We want to create an app that uses the details of a current location (presumably the user's) to discover a list of places within a set radius. Users would have the ability to discover and explore nearby places.

The query is the magic wand, letting users discover and explore nearby places based on their current location and a set radius. Thanks to MongoDB's geospatial features, we're talking about storing and fetching location-based data with style and efficiency.

Our journey into geospatial features in MongoDB spans these key areas:

  1. Setting up your data model: Lay the foundation right.

  2. Creating and storing Geospatial data: Get your hands dirty with data.

  3. Constructing Geospatial queries: Unleash the power of queries.

So, are you ready to rock the geospatial world of MongoDB? Let's get started!

{
  "places": [
    {
      "name": "Lekki Conservation Centre",
      "type": "Nature Reserve",
      "location": {
        "type": "Point",
        "coordinates": [3.5683, 6.4698]
      }
    },
    {
      "name": "Nike Art Gallery",
      "type": "Art Gallery",
      "location": {
        "type": "Point",
        "coordinates": [3.5479, 6.4432]
      }
    },
    {
      "name": "Eko Atlantic City",
      "type": "City",
      "location": {
        "type": "Point",
        "coordinates": [3.4064, 6.4276]
      }
    }
  ]
}

Above is a sample output of what our app would dish out, showing some cool places around Lagos, Nigeria. Each place has a name, a type, and a location, which is a GeoJSON object with a type and coordinates. The app will return places that are within a given radius from a given point, all thanks to MongoDB’s geospatial query wizardry.

Setting up the Data Model

Before we can store and fetch geospatial data in MongoDB, let's lay the foundation by setting up our data model. Think of a data model as the architect's blueprint, organizing and structuring the data in a way that suits the application's needs. In MongoDB, data is stored in collections – which are basically groups of documents. And what are documents? Well, they are JSON-like objects that can have various fields and values, including nested objects and arrays.

Using the GeoJSON Format

Now, onto the fun part. When we talk about geospatial data representation in our documents, MongoDB gives us two choices: GeoJSON or legacy coordinate pairs. For our tutorial, we're taking the modern route with GeoJSON because it's sleek, standardized, and more compatible with other geospatial tools libraries.

GeoJSON is a standard format for encoding geospatial data based on JSON. It includes several geospatial objects – points, lines, polygons, and geometries. Each object has a type field and a coordinates field, which contains an array of numbers that pinpoint the object's location. For instance, a point object has a type "Point" and coordinates of [longitude, latitude].

Now, let's weave this GeoJSON magic into our data model and get ready for the geospatial adventures ahead!

Quick example; herein lies the GeoJSON point with the coordinates of the Eiffel Tower:

{
    type: "Point",
    coordinates: [48.8584, 2.2945]
}

You can check it by pasting the coordinate points into the search bar on Google Maps, and you get to see the amazing tower. Pretty cool, right?

Creating a Geospatial Index

Now, let's talk about the superpowered geospatial index. This nifty tool is what enables us to perform geospatial queries on our GeoJSON-packed field. MongoDB offers us two types of geospatial indexes: 2dsphere and 2d. The 2dsphere index is the star of our show, as it supports queries that calculate geometries on the spherical Earth. On the other hand, the 2d index works on a flat plane.

For our tutorial on uncovering nearby places on Earth's surface, we're rolling with the 2dsphere index. Creating this magic index is rather easy using MongoDB's db.collection.createIndex() method. We just specify the index type as "2dsphere," and voila! For instance, check out how we weave this enchantment on the location field of our places collection:

db.places.createIndex({ location: "2dsphere" })

With this index in place, our MongoDB database is now geared up to handle geospatial queries. Ready to unleash the magic? Let's keep the geospatial journey rolling!

Choosing our Query Operator

Now, let's dive into a geospatial query operator that's like our own digital compass – for example, the $nearSphere operator. The mission of this operator is to pick out documents with GeoJSON points that are near a specified spot on Earth's surface.

The $nearSphere operator plays nice with a GeoJSON point, and you can even throw in a max distance and a minimum distance for extra precision. What does it do? It grabs documents that fit the query and arranges them by their distance from the chosen point.

Here's a real-world scenario: let's say we want to uncover all the cool places within 10000 meters of the Eiffel Tower. The $nearSphere operator swings into action:

db.places.find({
  location: {
    $nearSphere: {
      $geometry: {
        type: "Point",
        coordinates: [2.2945, 48.8584]
      },
      $maxDistance: 10000
    }
  }
})

This magic query pulls out all the documents in the places collection where the GeoJSON point in the location field is within 10000 meters of the Eiffel Tower. And the bonus? It sorts them by distance. Ready to wield the $nearSphere magic in your geospatial queries? Let the exploration continue!

Defining our Model Schema with Mongoose

Now, let's sketch out the blueprint for our places collection using the marvelous Mongoose. Mongoose is an elegant Node.js library that provides a straightforward way to interact with MongoDB, a popular NoSQL database. It acts as an Object Data Modeling (ODM) tool, translating data between JavaScript objects in code and the JSON-like documents stored in MongoDB.

To get started, we import the Mongoose module and craft a shiny new mongoose.Schema object. Think of this as our instruction manual for how our collection should look.

Here's the code magic:

const mongoose = require("mongoose");

const placeSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true
    },
    placeType: {
        type: String,
        enum: ["Restaurant", "Hospital", "Gym", "School", "Mall"],
        required: true
    },
    location: {
        type: {
            type: String,
            default: "Point"
        },
        city: {
            type: String, 
            required: true
        },
        coordinates: {
            type: [Number],
            index: '2dsphere'
        }
    }
});

// Create the index and tie it to the location field
placeSchema.index({ location: '2dsphere' });

// Export model schema
module.exports = mongoose.model("Place", placeSchema);

Breaking it down:

  • name and placeType are mandatory strings. placeType is restricted to predefined values like "Restaurant," "Hospital," etc.

  • location is the star of the show. It's a GeoJSON point with a type, city, and coordinates.

  • We set a default of "Point" for the GeoJSON type.

  • Coordinates hold the long and lat, paving the way for our geospatial adventures.

  • A slick 2dsphere index on the location field opens up the world of geospatial queries.

With this Mongoose schema in hand, we're ready to roll with a structured and geospatially savvy places collection. Onwards with the journey!

Connecting to the Database

Now let's use the power of Mongoose to connect to the MongoDB database and set the path for our geospatial explorations. Mongoose makes it a breeze to define, validate, and play around with our data, especially when it comes to geospatial wonders.

Check out the beautiful code:

// Import mongoose
const mongoose = require("mongoose");

// Define the connection string
// The name geo here is going to be the name of my database,
// you can change it into something else.
const connectionString = "mongodb://localhost:27017/geo";

// Connect to the database
mongoose.connect(connectionString);

// Initialize the db
const db = mongoose.connection

db.on("error", console.error.bind(console, "Connection error:"));
db.once("open", () => console.log("Connected to the database"));

Breaking it down:

  • We import the mongoose module, our go-to for MongoDB interaction.

  • The connectionString holds the key to our MongoDB server, specifying the protocol, host, port, and database name. Feel free to include your username and password for authentication if needed. The one being used is a local database and doesn't need authentication (Anyone can use mongodb locally with that exact connection string, except for the /geo, that's dynamic)

  • The mongoose.connect() method initiates the connection to the specified MongoDB database using the provided connection string.

  • The db.on("error", ...) event listener is set up to catch any errors that might occur during the connection process.

  • The db.once("open", ...) event listener celebrates a successful connection by logging a message to the console.

With this, our app is now officially plugged into the MongoDB realm. Ready to start creating and querying geospatial data? Let the geospatial journey continue!

Creating and Storing Geospatial Data:

Now that we've laid the groundwork with our data model, let's jump into the exciting part – creating and storing geospatial data in MongoDB using our Mongoose model.

Check out the code:

// Import the model, be sure to use the correct file directory of your model
const Place = require("./place");
// Variable for stopping infinite execution of sample places
let sampleDataInitialized = false;

// Create some sample data
const places = [
  {
    name: "Pizza Hut",
    placeType: "Restaurant",
    location: {
      city: "Lagos",
      coordinates: [3.3958, 6.4531]
    }
  },
  {
    name: "Lagoon Hospital",
    placeType: "Hospital",
    location: {
      city: "Lagos",
      coordinates: [3.4215, 6.4413]
    }
  },
  {
    name: "Bodyline Fitness",
    placeType: "Gym",
    location: {
      city: "Lagos",
      coordinates: [3.4156, 6.4326]
    }
  },
  {
    name: "University of Lagos",
    placeType: "School",
    location: {
      city: "Lagos",
      coordinates: [3.4064, 6.5196]
    }
  },
  {
    name: "Ikeja City Mall",
    placeType: "Mall",
    location: {
      city: "Lagos",
      coordinates: [3.3569, 6.6018]
    }
  }
];

// Function to stop infinite execution on places
const initializeSampleData = async () => {
    try {
        await Place.deleteMany();
        await Place.create(places);
        console.log('sample data initialized');
        sampleDataInitialized = true;
    } catch (error) {
        console.error('Error initializing sample data: ', error.message);
    }
}

if (!sampleDataInitialized) {
    initializeSampleData();
}

Breaking it down:

  • We import our model, the mighty Place.

  • The array "places" is a set of places we would be using for demonstration in this example, it follows the rules of our model schema, with names, place types, and locations.

  • The initializeSampleData function is defined to asynchronously clear existing data in the Place model and insert the new sample data. If successful, it logs a message. (This is done to prevent the infinite creation of the "places" array contents)

  • The sampleDataInitialized variable is used to control the execution of the sample data initialization. If it's not initialized, the initializeSampleData function is called.

Run this code in your Node.js application, and voila! Your database is now populated with some geospatial wonders. Ready to explore and query this data? Let the geospatial journey unfold!

Constructing Geospatial Queries

Now that we've sprinkled our MongoDB with geospatial wonders, let's unleash the power of geospatial query operators on our data. MongoDB's geospatial query operators allow us to play with distance, proximity, and containment criteria.

Let's dive into some exciting examples:

Finding the Nearest Places to a Point:

To uncover the hidden gems near a given point, say [3.4, 6.5], we summon the $nearSphere operator:

async function discover() {
    try {
        // Mongoose query for finding all places within 10000 metres of point
        const query = {
            location: {
                $nearSphere: {
                    $geometry: {
                        type: "Point",
                        coordinates: [3.4, 6.5]
                    },
                    $maxDistance: 10000
                }
            }
        }

        const nearbyPlaces = await Place.find(query);
        console.log("Here are the places close to you: ", nearbyPlaces);
    } catch (error) {
        console.error("error discovering nearby places", error.message);
    }
}

discover();

This magical discover() function returns all documents in the places collection with a location field within 10000 meters of the point [3.4, 6.5]. The results are gracefully sorted by distance, from the nearest to the farthest.

Finding All Places Within a Certain Area:

Now, let's expand our horizons and find all places within a circular area. We invoke the $geoWithin operator:

async function rediscover() {
    try {
        // Mongoose query for finding all places within a 5 kilometer radius of point
        const query = {
            location: {
                $geoWithin: {
                    $centerSphere: [
                        [3.4, 6.5],
                        5 / 6378.1
                    ]
                }
            }
        }

        const nearbyPlaces = await Place.find(query);
        console.log("Here are the places within a 5 kilometer radius: ", nearbyPlaces);
    } catch (error) {
        console.error("error discovering nearby places", error.message);
    }
}

rediscover()

This enchantment returns all documents in the places collection with a location field fully within the circular area. The radius, in radians, is calculated by dividing the distance in kilometers by the Earth's radius in kilometers (6378.1).

With these geospatial query operators, our journey through the geospatial wonders of MongoDB is now concluded. But, you can always go through the mongoDB documentation or the general internet for more query operators.

Our Query Responses

Due to how simple our project is, our responses from both queries could be quite similar, or not, really depends on how you structured it, here is what mine looks like;

// This is the response from my rediscover() function that uses
// the "$geoWithin" query operator
Here are the places within a 5 kilometer radius:  [
  {
    location: { 
        city: 'Lagos', 
        coordinates: [Array], 
        type: 'Point' 
    },
    _id: new ObjectId('659705e06eecc526bf1a7d17'),
    name: 'University of Lagos',
    placeType: 'School',
    __v: 0
  }
]

// This is the response from my discover() function that uses 
// the "$nearSphere" query operator
Here are the places close to you:  [
  {
    location: { 
        city: 'Lagos', 
        coordinates: [Array], 
        type: 'Point' 
    },
    _id: new ObjectId('659707bc2018eb03fcdffd44'),
    name: 'University of Lagos',
    placeType: 'School',
    __v: 0
  },
  {
    location: { 
        city: 'Lagos', 
        coordinates: [Array], 
        type: 'Point' 
    },
    _id: new ObjectId('659707bc2018eb03fcdffd41'),
    name: 'Pizza Hut',
    placeType: 'Restaurant',
    __v: 0
  },
  {
    location: { 
        city: 'Lagos', 
        coordinates: [Array], 
        type: 'Point' 
    },
    _id: new ObjectId('659707bc2018eb03fcdffd42'),
    name: 'Lagoon Hospital',
    placeType: 'Hospital',
    __v: 0
  }
]

Breaking it down:

  • Location Information:

    • City: the location of the place returned, which in this case is Lagos

    • Coordinates: The geographical representation is being represented as an array here, this is because there are several nested values in the response, but it contains two values; the longitude and latitude of the place returned.

  • _id: This is a unique identifier assigned by MongoDB to this document. It's an ObjectId type, you don't need to think too much about it, it always gets assigned automatically so you have no business with it, unless you need it for something else in your project.

  • name: The name of the place returned

  • placeType: The type of the place, which as we can see varies but are still all within the scope of what we defined in our data model schema

  • __v: This is added in by Mongoose for versioning the document, it is also done automatically and you have no business with it.

Summary

In this tutorial, we embarked on a journey into the realm of geospatial data with MongoDB. We delved into creating a robust data model using Mongoose, connecting to the MongoDB database, and then exploring the magic of geospatial queries.

Through the use of operators like $nearSphere and $geoWithin, we uncovered the ability to find the nearest places to a given point and identify all places within a specific area. Our model schema, defined with precision, allowed us to seamlessly integrate geospatial features into our MongoDB database.

Armed with the knowledge of geospatial queries, developers can enhance applications with location-based functionalities, from tracking delivery routes to providing real-time information tailored to specific locations.

As you continue your journey in the world of MongoDB and geospatial data, may your queries be precise, and your data adventures be ever-enchanting. Happy coding!

Extra Stuffs

To get the full code used for the tutorial, here is the link; https://github.com/lawwee/geo.

So, there you have it, I hope you have learned a thing or two about Geospatial Data and Queries. My name is Lawwee, and thank you for reading my post, you can connect with me via the information below, and send a DM if there is anything you’d like to talk about or ask.
Have a good day.

LinkedIn: https://www.linkedin.com/in/mohammed-lawal/
Twitter
: https://twitter.com/lawaldafuture1
Email
: lawalmohammed567@gmail.com