post

Build a GeoCoding feature for finding Users around in Nodejs api

By Olanrewaju A. Olaboye

||

18 October, 2020

In recent times it has seemed like a magic that social media applications can show you users they believe live around you and you might probably want to connect with. As a developer at one point in time you will need to track location of users and recommend people they may know to establish friend connection/network.

In this tutorial I am going to be showing us exactly how to track last login location of a user, save/update this in our database and use this reservoir of data to recommend users around for each user on the platform.

Prerequisites

  1. Basic knowledge of Nodejs, express framework and mongoose ORM
  2. Knowledge of Token based auth using JWT in nodejs

Guide

  1. Clone and Test the boilerplate code
  2. Write mongodb/mongoose schema with 2dsphere index.
  3. Install, configure and use express-ip to grab location users made http request from.
  4. Query user model for other users around a particular user.

Clone and Test the boilerplate code

1. git clone https://github.com/WonderfulOlanrewaju/nearby-api-boilerplate.git
2. cd /nearby-api-boilerplate
3. yarn install && yarn add cross-env 

The three commands above, command one clones the remote repo into your current working directory. Command two changes directory into the copied folder while command tree install all dependencies needed to run the boilerplate code with signup and login with JWT already done.

create a .env file in the root of your project and add the environment variable

secretKey = yoursecretkeyvalue

Make a sample Register request to your api via postman

Alt Text

Make a sample Login request to your api via postman Alt Text

Write mongodb/mongoose schema with 2dsphere index.

Create a file: src/controllers/utils/updateLocation.js

import { User } from "../../models/User.model";

export const UpdateLastLocation = async (ipInfo, userId) => {
  let lastLocation = {
    type: "Point",
    coordinates: ipInfo.ll,
  };
  let savedUser = await User.findByIdAndUpdate(
    userId,
    {
      ipInfo,
      lastLocation,
    },
    { new: true }
  );

  return savedUser;
};

This is a utility function which will be used on any route we want to track location of user to extract their logitude and latitude and save it into some fields in the user object from the DB.

The next step is to update our user model to allow us save ipInfo and lastLocation of user into the DB.

Create a file src/models/User.model.js

import Mongoose from "mongoose";
import autoIncrement from "mongoose-auto-increment";
let { connection, Schema } = Mongoose;
autoIncrement.initialize(connection);

const pointSchema = new Schema({
  type: {
    type: String,
    enum: ["Point"],
    required: true,
  },
  coordinates: {
    type: [Number],
    required: true,
  },
});

const UserSchema = new Schema({
  firstName: {
    type: String,
    min: 2,
    default: "",
  },
  lastName: { type: String, default: "" },
  email: { type: String, unique: true },
  address: { type: String, default: "" },
  password: { type: String, default: "" },
  ipInfo: {
    ip: { type: String, default: "" },
    range: { type: Array, default: [] },
    country: { type: String, default: "" },
    region: { type: String, default: "" },
    eu: { type: String, default: "" },
    city: { type: String, default: "" },
    ll: { type: Array },
    metro: Number,
    area: Number,
  },
  lastLocation: {
    type: pointSchema,
    default: {
      type: "Point",
      coordinates: [0, 0],
    },
    index: "2dsphere",
  },
});

UserSchema.plugin(autoIncrement.plugin, {
  startAt: 1,
  incrementBy: 1,
  model: "User",
});

export const User = Mongoose.model("User", UserSchema);

What we did above is to update the user model for new fields that allow us save ipInfo and last location of users into the DB.

Install, configure and use express-ip to grab location users made http request from.

What the package allows us to do is discover the longitude and latitude a user made a request from alongside some other details like city, timezone and country based on their ip-address.

yarn add express-ip

Inside add new codes src/app.js

//to the upper part before app.get("/")
import { User } from "./models/User.model";
import expressIP from "express-ip";
app.use(expressIP().getIpInfoMiddleware);

//To the lower part before the last line of code add :
app.get("/nearbyusers", async (req, res) => {
  try {
    const { ipInfo } = req;
    let nearByUsers = await User.find({
      lastLocation: {
        $nearSphere: {
          $geometry: {
            type: "Point",
            coordinates: ipInfo.ll,
          },
          $maxDistance: 10000,
        },
      },
    });
    if (!nearByUsers || nearByUsers.length === 0) {
      res.status(201).json({
        message: "No users near you",
        nearByUser: [],
      });
    } else {
      res.status(201).json({
        message: "Here are users near you",
        nearByUsers,
      });
    }
  } catch (err) {
    res.status(400).json({
      message: `Issues finding nearby users. ${err.message}`,
    });
  }
});

what we have done is import express-ip package and configure it to be available on all routes of our app. And write a route that basically checks the ipInfo of the person calling it then send them an array of users based on 10Km proximity.

Add a new route to your auth controller for fetching a user detail What we want to use this route for is to update the last location and ip details of where a user made his last request from.

src/controllers/major/auth.controller.js

import { createUser, loginUser } from "../utils/User.util";
import { handleResError, handleResSuccess } from "../utils/response.util";
import JWT from "jsonwebtoken";
import dotenv from "dotenv";
dotenv.config();
const { secretKey } = process.env;

export const SignupController = async (req, res) => {
  try {
    let userDetails = req.body;
    let { err, user } = await createUser(userDetails);
    if (err) handleResError(res, err, 400);
    else {
      let { _id, email, isActive } = user;
      let options = {
        expiresIn: "12h",
        issuer: "nearby-hasher",
      };
      let token = await JWT.sign({ _id, email, isActive }, secretKey, options);
      handleResSuccess(res, `Account created!`, token, 201);
    }
  } catch (err) {
    handleResError(res, err, 400);
  }
};

export const LoginController = async (req, res) => {
  try {
    let { err, token } = await loginUser(req.body);
    if (err) handleResError(res, err, 400);
    else handleResSuccess(res, "login successful", token, 201);
  } catch (err) {
    handleResError(res, err, 400);
  }
};

export const FetchAUserController = async (req, res) => {
  try {
    console.log(req.decoded);
    const { ipInfo } = req;
    let id = req.decoded._id;
    let updatedUser = await UpdateLastLocation(ipInfo, id);
    handleResSuccess(res, "user fetched", updatedUser, 201);
  } catch (err) {
    handleResError(res, err, 400);
  }
};

What is done is basically calling our updateLastLocation function supply it the id and ipInfo of user so this will save the user location details into the db.

Test the user fetch route ensure to add auth token as Authorization header to the request as in the screenshot below. So the ipInfo of the user can be saved in the DB

Alt Text

Update User location on Signup/Login src/controllers/majors/auth.controller.js

import { createUser, loginUser } from "../utils/User.util";
import { handleResError, handleResSuccess } from "../utils/response.util";
import JWT from "jsonwebtoken";
import { User } from "../../models/User.model";
import dotenv from "dotenv";
import { UpdateLastLocation } from "../utils/updateLastLocation";
dotenv.config();
const { secretKey } = process.env;

export const SignupController = async (req, res) => {
  try {
    let userDetails = req.body;
    let ipInfo = { req };
    let { err, user } = await createUser(userDetails);
    if (err) handleResError(res, err, 400);
    else {
      let { _id, email, isActive } = user;
      await UpdateLastLocation(ipInfo, _id);
      let options = {
        expiresIn: "12h",
        issuer: "nearby-hasher",
      };
      let token = await JWT.sign({ _id, email, isActive }, secretKey, options);
      handleResSuccess(res, `Account created!`, token, 201);
    }
  } catch (err) {
    handleResError(res, err, 400);
  }
};

export const LoginController = async (req, res) => {
  try {
    let ipInfo = { req };
    let { err, token } = await loginUser(req.body);
    let user = await User.findOne({ email: req.body.email });
    await UpdateLastLocation(ipInfo, user._id);
    if (err) handleResError(res, err, 400);
    else handleResSuccess(res, "login successful", token, 201);
  } catch (err) {
    handleResError(res, err, 400);
  }
};

export const FetchAUserController = async (req, res) => {
  try {
    console.log(req.decoded);
    const { ipInfo } = req;
    let id = req.decoded._id;
    let updatedUser = await UpdateLastLocation(ipInfo, id);
    handleResSuccess(res, "user fetched", updatedUser, 201);
  } catch (err) {
    handleResError(res, err, 400);
  }
};

Add start script to package.json and push app to heroku because the live testing of the nearbyusers route needs to be on a live server

package.json

"start": "node -r esm ./src/server.js"

Push to heroku

  1. Ensure you have your heroku CLI loggedIn
  2. Ensure you add your live mongodb Database connection strin
  3. Type in the following commands one after the other on your terminal.

You can compare your work with my Final Repo

heroku create 
git add .
git commit -m 'heroku push
git push heroku master || git push heroku main

Reason for adding git push heroku main is because master is now renamed as main in recent Github repo creation.

Visit the documentation on Postman Documentation

  1. Create two users on the live API with different emails
  2. Call the fetch user route
  3. Then on another browser visit LiveAPI

It will show you the two users you created because they live around you.

This is my first tutorial, kindly comment things to improve on in the comment section.