Geospatial data is becoming increasingly important in a variety of applications, from mapping and navigation to location-based services and geospatial analytics. MongoDB, with its support for geospatial indexes and queries, provides an excellent platform for storing and querying geographical data. In this article, we’ll dive into MongoDB’s geospatial features and show how you can leverage them with Node.js.
Before we explore the queries, we need to populate our MongoDB collection with geospatial data. I have uploaded the necessary JSON files: sl-places.json
and sl-areas.json
to my GitHub repository. These files contain geographical data for various places and areas in Sri Lanka, respectively.
First, let’s write a seeder script to load this data into MongoDB. You can find the full code in My repository:
const fs = require("fs");
const { getDB, closeDB } = require("./db");
(async () => {
const initialSLPlaces = JSON.parse(
fs.readFileSync("sl-places.json", "utf-8")
);
const initialSLAreas = JSON.parse(fs.readFileSync("sl-areas.json", "utf-8"));
const db = await getDB();
// Delete existing records to avoid duplication
await db.collection("sl-places").deleteMany();
// Insert new data into the collection
await db
.collection("sl-places")
.insertMany([...initialSLPlaces, ...initialSLAreas]);
// Create a 2dsphere index to enable efficient geospatial queries
await db.collection("sl-places").createIndex({ location: "2dsphere" });
// Close the database connection
closeDB();
})();
sl-places.json
and sl-areas.json
into the sl-places
collection.location
field to enable efficient geospatial queries.Indexes are critical for improving the performance of database queries, especially when dealing with large datasets. In the context of geospatial data, MongoDB uses 2dsphere indexes to support queries that deal with spherical geometry, such as finding points within a certain distance from a location or finding points within a polygon.
Without a 2dsphere index on the location
field, geospatial queries would be much slower, as MongoDB would need to scan every document in the collection to perform the query. By creating a 2dsphere index, MongoDB can efficiently query geospatial data by using the index to quickly find relevant documents based on their geographic location.
One of the most common geospatial queries is finding places that are near a specific location. In MongoDB, we can use the $near
operator to perform this search. The following Node.js code demonstrates how to search for places within 500 meters of Kirinda Beach (coordinates: [81.2570, 6.2155]
).
const { getDB, closeDB } = require("./db");
(async () => {
const db = await getDB();
// Search near Kirinda Beach within 500m
const places = await db
.collection("sl-places")
.find({
location: {
$near: {
$geometry: {
type: "Point",
coordinates: [81.2570, 6.2155], // Coordinates for Kirinda Beach
},
$maxDistance: 500, // Max distance in meters
},
},
})
.toArray();
console.log(places); // Display the found places
closeDB(); // Close the database connection
})();
$near
operator finds documents within a specified distance from a point. In this case, we are searching for places within 500 meters of the given coordinates.This query is ideal for applications that need to find nearby places, such as a location-based service for tourists, or a navigation app that shows nearby points of interest.
In addition to searching near a location, MongoDB allows you to search for places within a specific geographic area defined by a polygon. This can be useful for querying places that lie within a predefined region or boundary.
The following example demonstrates how to search for places within a rectangular polygon defined by four corners:
const { getDB, closeDB } = require("./db");
(async () => {
const db = await getDB();
const places = await db
.collection("sl-places")
.find({
location: {
$geoWithin: {
$geometry: {
type: "Polygon",
coordinates: [
[
[81.2, 6.2], // Bottom-left corner
[81.6, 6.2], // Bottom-right corner
[81.6, 6.5], // Top-right corner
[81.2, 6.5], // Top-left corner
[81.2, 6.2], // Closing the polygon
],
],
},
},
},
})
.toArray();
console.log(places); // Display places within the polygon
closeDB(); // Close the database connection
})();
$geoWithin
operator is used to find documents within a specified geometry, which can be a polygon, circle, or other shapes.This query is particularly useful for applications that need to filter results within a geographical region, such as finding all points of interest within a park, city, or administrative region.
Another geospatial operator in MongoDB is $geoIntersects
, which allows you to search for areas that intersect with a specific point. This is useful for cases where you need to find out which regions or zones contain a given location.
Below is an example of how to search for areas that intersect with the point [81.7302, 7.2801]
:
const { getDB, closeDB } = require("./db");
(async () => {
const db = await getDB("playground");
const area = await db
.collection("sl-places")
.find({
area: {
$geoIntersects: {
$geometry: { type: "Point", coordinates: [81.7302, 7.2801] },
},
},
})
.toArray();
console.log(area); // Display areas that intersect with the point
closeDB(); // Close the database connection
})();
$geoIntersects
operator finds documents whose geometry intersects with the specified geometry.This query can be used to determine which administrative regions or zoning areas intersect with a given point, such as determining which district a particular location belongs to.
MongoDB’s geospatial capabilities make it an excellent choice for applications that deal with location-based data. By using the 2dsphere index and geospatial operators like $near
, $geoWithin
, and $geoIntersects
, developers can build powerful, location-aware applications.
In this article, we’ve demonstrated how to perform basic geospatial queries with MongoDB and Node.js. We also covered the importance of creating a 2dsphere index, which is crucial for ensuring fast and efficient geospatial queries on large datasets.
By integrating MongoDB’s geospatial queries into your application, you can create more intelligent, context-aware experiences for your users.
// Checkout complete code in GitHub, Don’t forget to give me a star :)
sl-places.json
and sl-areas.json
to my GitHub repository. These files contain geographical data for various places and areas in Sri Lanka, respectively.First, let’s write a seeder script to load this data into MongoDB. You can find the full code in My repository:const fs = require("fs");
const { getDB, closeDB } = require("./db");
(async () => {
const initialSLPlaces = JSON.parse(
fs.readFileSync("sl-places.json", "utf-8")
);
const initialSLAreas = JSON.parse(fs.readFileSync("sl-areas.json", "utf-8"));
const db = await getDB();
// Delete existing records to avoid duplication
await db.collection("sl-places").deleteMany();
// Insert new data into the collection
await db
.collection("sl-places")
.insertMany([...initialSLPlaces, ...initialSLAreas]);
// Create a 2dsphere index to enable efficient geospatial queries
await db.collection("sl-places").createIndex({ location: "2dsphere" });
// Close the database connection
closeDB();
})();
sl-places.json
and sl-areas.json
into the sl-places
collection.location
field to enable efficient geospatial queries.location
field, geospatial queries would be much slower, as MongoDB would need to scan every document in the collection to perform the query. By creating a 2dsphere index, MongoDB can efficiently query geospatial data by using the index to quickly find relevant documents based on their geographic location.$near
operator to perform this search. The following Node.js code demonstrates how to search for places within 500 meters of Kirinda Beach (coordinates: [81.2570, 6.2155]
).const { getDB, closeDB } = require("./db");
(async () => {
const db = await getDB();
// Search near Kirinda Beach within 500m
const places = await db
.collection("sl-places")
.find({
location: {
$near: {
$geometry: {
type: "Point",
coordinates: [81.2570, 6.2155], // Coordinates for Kirinda Beach
},
$maxDistance: 500, // Max distance in meters
},
},
})
.toArray();
console.log(places); // Display the found places
closeDB(); // Close the database connection
})();
$near
operator finds documents within a specified distance from a point. In this case, we are searching for places within 500 meters of the given coordinates.const { getDB, closeDB } = require("./db");
(async () => {
const db = await getDB();
const places = await db
.collection("sl-places")
.find({
location: {
$geoWithin: {
$geometry: {
type: "Polygon",
coordinates: [
[
[81.2, 6.2], // Bottom-left corner
[81.6, 6.2], // Bottom-right corner
[81.6, 6.5], // Top-right corner
[81.2, 6.5], // Top-left corner
[81.2, 6.2], // Closing the polygon
],
],
},
},
},
})
.toArray();
console.log(places); // Display places within the polygon
closeDB(); // Close the database connection
})();
$geoWithin
operator is used to find documents within a specified geometry, which can be a polygon, circle, or other shapes.[81.7302, 7.2801]
:const { getDB, closeDB } = require("./db");
(async () => {
const db = await getDB("playground");
const area = await db
.collection("sl-places")
.find({
area: {
$geoIntersects: {
$geometry: { type: "Point", coordinates: [81.7302, 7.2801] },
},
},
})
.toArray();
console.log(area); // Display areas that intersect with the point
closeDB(); // Close the database connection
})();
$geoIntersects
operator finds documents whose geometry intersects with the specified geometry.$near
, $geoWithin
, and $geoIntersects
, developers can build powerful, location-aware applications.npx @react-native-community/cli@latest init exampleProject
android
directory of the project,I navigated to the java/com/exampleproject
folder and created a new Kotlin class, CounterModule.kt
. Below is the implementation:package com.exampleproject
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.Promise
class CounterModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
private var counter = 0
override fun getName(): String {
// The name we can access inside native modules
return "Counter"
}
// Expose increment method
@ReactMethod
fun increment(callback: Callback) {
counter++
// Call the callback with the updated counter value
callback.invoke(counter)
}
// Expose decrement method as a Promise
@ReactMethod
fun decrement(promise: Promise) {
if (counter > 0) {
counter--
promise.resolve(counter) // Resolve the promise with the updated counter value
} else {
promise.reject("COUNTER_ERROR", "Counter value cannot be less than 0")
}
}
}
CounterModule
class that includes two methods:increment
: Increases the counter value and returns it via a callback.decrement
: Decreases the counter value if it is greater than 0 and resolves it as a promise. If the counter is already at 0, it rejects the promise with an error.package com.exampleproject
import android.view.View
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
// Module register import
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager
// Extend ReactPackage to register module
class CounterPackage : ReactPackage {
override fun createNativeModules(
reactContext: ReactApplicationContext
): MutableList<NativeModule> = listOf(CounterModule(reactContext)).toMutableList()
override fun createViewManagers(
reactContext: ReactApplicationContext
): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()
}
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
add(CounterPackage())
}
// Counter.swift
// exampleProject
import Foundation
// This to make sure to export these class/function to object c runtime
@objc(Counter)
class CounterModule: NSObject {
private var count = 0
// _ is to get the first param, callback to get the second param
@objc
func increment(_ callback: RCTResponseSenderBlock) {
count += 1
// print(count);
callback([count])
}
@objc
func decrement(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
if count == 0 {
let error = NSError(domain: "Counter", code: 200, userInfo: nil)
reject("ERROR_COUNT", "count cannot be negative", error)
} else {
count -= 1
resolve(count)
}
}
// This means we are asking React Native to initialize these modules before the JS main thread starts executing
// If returns false, this means it's okay if we initialize the module in the background thread
@objc
static func requiresMainQueueSetup() -> Bool {
return true
}
}
increment: Increases the counter and returns the new value to JavaScript via a callback.
decrement: Decreases the counter, returning a promise to resolve the new value or reject it with an error if the counter is already 0.
Counter.m
file in the iOS project:// Counter.m
// exampleProject
#import <Foundation/Foundation.h>
// This will help us export the function to React Native
#import "React/RCTBridgeModule.h"
// Expose the Counter object
@interface RCT_EXTERN_MODULE(Counter, NSObject)
// Expose increment method
RCT_EXTERN_METHOD(increment : (RCTResponseSenderBlock)callback)
// Expose decrement promise
RCT_EXTERN_METHOD(decrement : (RCTPromiseResolveBlock)resolve
reject : (RCTPromiseRejectBlock)reject)
@end
import { NativeModules } from 'react-native';
const { Counter } = NativeModules;
// Increment the counter
Counter.increment((newCounterValue) => {
console.log(`Counter incremented: ${newCounterValue}`);
});
// Decrement the counter
Counter.decrement()
.then((newCounterValue) => {
console.log(`Counter decremented: ${newCounterValue}`);
})
.catch((error) => {
console.error(`Error: ${error.message}`);
});
npx @react-native-community/cli@latest init exampleProject
package com.exampleproject
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.Promise
class CounterModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
private var counter = 0
override fun getName(): String {
// The name we can access inside native modules
return "Counter"
}
// Expose increment method
@ReactMethod
fun increment(callback: Callback) {
counter++
// Call the callback with the updated counter value
callback.invoke(counter)
}
// Expose decrement method as a Promise
@ReactMethod
fun decrement(promise: Promise) {
if (counter > 0) {
counter--
promise.resolve(counter) // Resolve the promise with the updated counter value
} else {
promise.reject("COUNTER_ERROR", "Counter value cannot be less than 0")
}
}
}
CounterModule
class that includes two methods:increment
: Increases the counter value and returns it via a callback.decrement
: Decreases the counter value if it is greater than 0 and resolves it as a promise. If the counter is already at 0, it rejects the promise with an error.npx @react-native-community/cli@latest init exampleProject
import webdriver
driver = webdriver.Chrome()
driver.get("https://example.com")
assert driver.getTitle().contains("Example Domain");
driver.quit()
pipeline {
stages {
stage('Test') {
steps {
sh 'pytest tests/'
}
}
}
}
import pytest
@pytest.mark.parametrize("username,password", [("user1", "pass1"), ("user2", "pass2")])
def test_login(username, password):
assert login(username, password) == "Success"
fake = Faker()
print(fake.email())
@prisma/adapter-pg
and AWS Secrets Manager on the NestJS project.Additionally, we’ll explore deployment considerations, including handling Prisma migrations and seed operations during the build and deploy stages, ensuring that your application runs smoothly in production.@prisma/adapter-pg
: Allows integration with the pg
library, giving fine-grained control over connection pooling.pg
's connection pooling capabilities to dynamically fetch credentials and manage connections.@prisma/adapter-pg
adapter and support dynamic connection management.npm install @prisma/adapter-pg @aws-sdk/client-secrets-manager pg
driverAdapters
preview feature in schema.prisma
:generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
npx prisma generate
import { Injectable } from '@nestjs/common';
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
@Injectable()
export class SecretsService {
private secretsManagerClient: SecretsManagerClient;
constructor() {
this.secretsManagerClient = new SecretsManagerClient({
region: 'YOUR_AWS_REGION',
});
}
// Fetch database URL from AWS Secrets Manager
async getDatabaseUrl(): Promise<string> {
try {
const secretId = 'YOUR_AWS_SECRET_ID';
const command = new GetSecretValueCommand({ SecretId: secretId });
const secret = await this.secretsManagerClient.send(command);
if (!secret.SecretString) {
throw new Error('SecretString is empty');
}
const credentials = JSON.parse(secret.SecretString);
return credentials.password;
} catch (error) {
throw error;
}
}
}
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { SecretsService } from '../secrets/secrets.service';
import { Pool } from 'pg';
@Injectable()
export class DatabaseService extends PrismaClient implements OnModuleInit {
private pool: Pool;
constructor(
private readonly secretsService: SecretsService,
) {
// Dynamically fetch the password and set up the pg Pool
const pool = new Pool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
database: process.env.DB_NAME,
port: 5432,
password: async () => {
return await this.secretsService.getDatabaseUrl();
},
ssl: { rejectUnauthorized: false },
});
const adapter = new PrismaPg(pool);
super({ adapter });
}
// Connect to the database
async onModuleInit() {
try {
await this.$connect();
} catch (error) {
throw error;
}
}
// Close the database connection pool when the application shuts down
async onModuleDestroy() {
try {
await this.pool.end();
} catch (error) {
throw error;
}
}
}
ssl: { rejectUnauthorized: false }
?rejectUnauthorized
option determines whether the client verifies the database server's SSL certificate. By setting rejectUnauthorized: false
, the client skips this validation, which can be helpful during local development or testing when certificates might not be properly configured. However, in production, it's better to ensure secure communication by using properly configured SSL certificates and enabling rejectUnauthorized: true
. This prevents man-in-the-middle attacks and ensures the authenticity of the database server.rejectUnauthorized
is set to false
SecretsService
ensures the latest credentials are always fetched.pg
's Pool
ensures connections finish gracefully before new credentials are used.pg
's pooling.DATABASE_URL="postgresql://user:$(encodeURIComponent 'password')@host:5432/dbname"
@prisma/adapter-pg
with AWS Secrets Manager and pg
, we solved the challenge of dynamic database credential rotation in Prisma. This approach ensures high availability, security, and scalability for modern applications.
Moreover it’s going to be a mess when we have to share state between non-connected components.
We can solve this problem by lifting user state up to the App component. But this can make our code messy when handling many states throughout the app.
For global state management we can use the React context API or libraries such as Redux or Recoil. In this post
In this post we do not go deeper into these topics and we will see how can we do this using unstated-next.
Unstated-next : Unstated-next is a simple light-weight library created based on React’s context API. We can use its simple API methods to manage global states throughout the app.
I’ll demonstrate the capabilities through a basic React app which has few components. Before proceeding, go ahead and create or clone a simple app.
Here in the app, Products.js and UserProfile.js represent separate pages and SideBar.js and TopBar.js components are used in both pages.