July 8, 2021
Build a location analytics reporting API in Node.js

Location, defined by latitude and longitude, can be used in conjunction with other data to generate insights for a business, which is known as location analytics.

Businesses that operate across the globe use location analytics across the value chain, for example, for locating users, delivering services, and running targeted ads. The use of location analytics has increased globally with the rise of social media and mobile devices.

In this tutorial, we’ll learn how to build a lightweight location analytics reporting service API in Node.js. At the end of the tutorial, you’ll be able to build this type of API for your own project. You’ll also have a better understanding of error handling and good file structure in Node.js!

Let’s get started!

Prerequisites

To follow along with this tutorial, you’ll need the following:

Familiarity with Node.js, Express, and Git
Visual Studio Code editor
Heroku account
Postman account

Setting up the file structure

First, we need to set up our file structure. Open your terminal and create a new directory where you’ll store all the files for the project. In your terminal, type the following command followed by the name of the folder, lars:

mkdir lars

Open the lars working directory in the VS Code editor:

code .

You’ll see your VS Code window open:

Visual Studio Code window

Initialize the working directory by opening your terminal in Visual Studio and running npm init -y.

If you wish to run this command outside of VS Code in your operating system’s terminal, navigate into the lars directory and run the command below:

npm init -y

The code above automatically generates the package.json file:

VS Code showing the package.json file created

In this tutorial, we’ll use Express as a dependency. Install Express by running the command below:

npm install express –save

Command to install Express as a dependency

After installing Express, you’ll notice that a node_modules folder was created. To confirm that you have Express installed, check your package.json file, and you’ll see Express installed as a dependency:

node_modules folder created and Express added to package.json

We need to import Express into our app because it is an npm module. Create a new file called app.js in the same directory as your package.json file:

Screenshot of VS Code window showing app.js created

In your app.js file,requireExpress by running the code below:

const express = require(‘express’);

Importing Express

Now, call Express to create your app, routes, and a port for your app to run on:

const app = express();

Node.js implements modularity, meaning it separates your app into modules, or various files, and exports each file. We’ll export app using the export keyword:

module.exports = app;

app.js file

Next, create another file called server.js in the same directory as the app.js file. Require the app.js file into the server.js file:

const app = require(‘./app’);

Create a file called config.env in the same directory as server.js. The config.env file will contain all the process.env keys we’ll need for our app. In the config.env file, create a PORT variable and set the PORT to listen on port 8000:

PORT=8000

After importing the app, create a constant called port in the server.js file. Set it to the PORT variable you just created and a default port of 3000:

const port = process.env.PORT || 3000;

Finally, we’ll set the app to listen on the port with the .listen() method:

app.listen(port, () => {
console.log(`App listening on ${port}`)
});

Building the routes

Whenever you visit a webpage or an application running on the web, you are making an HTTP request. The server responds with data from either the backend or a database, which is known as an HTTP response.

When you create a resource on a web app, you are calling the POST request. Similarly, if you try deleting or updating a resource on a web app, you are calling either a DELETE, PATCH, or UPDATE request. Let’s build routes to handle these requests.

Create a folder called routes in your working directory and create a file called analyticsRoute.js inside it. Require Express in the analyticsRoute.js file to set the route for the API:

const express = require(‘express’);

We also need to require our app module from the app.js file:

const app = require(‘../app’);

Then, we create our routes:

const router = express.Router();

Finally, we’ll export the router:

module.exports = router;

Building the controllers

We need to create files for controllers that we’ll import into our analyticsRoutes file. First, create a folder called controllers in your working directory.

Our API is going to use the IP address and coordinates provided by the user to calculate distance and location. Our request needs to accept that information and the request coming from the user.

We’ll use a POST request because the user is including data in the req.body. To save the information, we need to require an fs module (file system) in our controller.

Handling the POST request

Create a file named storeController.js in the controllers folder. In the storeController.js file, we need to import the fs module and the fsPromises.readFile() method to handle the promise returned, which is the user’s IP address and coordinates.

To install the fs module, open your terminal in your working directory and run the following command:

npm i fs –save

Type the following code at the top of your file:

const fsp = require(‘fs’).promises;
const fs = require(‘fs’);

Next, we’ll create the controller that will handle our route for the POST request. We’ll use the exports keyword and create an asynchronous middleware function that accepts three parameters:

req: stands for the request object

res: stands for the response object

next: function is called immediately after the middleware exports

postAnalytics = async(req, res, next) => {}

Now, we’ll save the properties of the data object in the req.body to the reportAnalytics array. We’ll set a Date() object to save the date any data was created in a createdAt key:

reportAnalytics.push({…req.body, createdAt: new Date()});

We’ll create a file called storeAnalytics.json to save the content of our reportAnalytics array as a string using JSON.stringify():

await fsp.writeFile(`${__dirname}/storeAnalytics.json`, JSON.stringify(reportAnalytics));

We need to check if the storeAnalytics.json file exists when a user makes a POST request. If the file exists, we need to read the file and save the output.

The output contains a constant called reportFile, which stores the content of the file that was read. Use JSON.parse on reportFile to convert the content of the file to a JavaScript object:

// checks if file exists
if (fs.existsSync(`${__dirname}/storeAnalytics.json`)) {
// If the file exists, reads the file
const reportFile = await fsp.readFile(`${__dirname}/storeAnalytics.json`, ‘utf-8’)
// converts the file to JavaScript Object
reportAnalytics = JSON.parse(reportFile)
} else {
// if file does not exist
return (‘File does not exist’);
}

The fs.existsSync() method synchronously checks if the file exists. It accepts the ${__dirname}/storeAnalytics.json path as its single parameter and points to the location of the file that we want to check.

We used the await keyword with reportFile to await the result from reading the file with the fsp.readFile() method. Next, we specified the path of the file we want to read with (${__dirname}/storeAnalytics.json. We set the encoding format to utf-8 , which will convert the content that is read from the file to a string.

JSON.parse() converts the reportFile to a JavaScript object and stores it in the reportAnalytics array. The code in the else statement block will run only when the file does not exist. Finally, we used the return statement because we want to stop the execution of the function after the code runs.

If the file was successfully read, created, and saved in the storeAnalytics.json file, we need to send a response. We’ll use the response object (res), which is the second parameter in our asynchronous postAnalytics function:

res.status(201).json({
status: ‘success’,
data: {
message: ‘IP and Coordinates successfully taken’
}
})

We’ll respond with a status of success and the data message IP and Coordinates successfully taken.

Your storeController.js file should look like the screenshot below:

Handling the GET request

We need to create another controller file to handle our GET request. When users make a GET request to the API, we’ll calculate their location based on their IP address and coordinates.

Create a file named fetchController.js in the controllers folder. In the storeController.js file, we need to require the fs module and the fsPromises.readFile() method to handle the promise returned:

const fsp = require(‘fs’).promises;
const fs = require(‘fs’);

Let’s create the controller to handle our route for the GET request. We’ll use a similar middleware function and parameters as we did for the POST request above:

exports.getAnalytics = async(req, res, next) => {}

Inside the getAnalytics middleware, type the following code to get the IP address from the query of the request:

const { ip } = req.query;

Now, create an empty array that will store the content of the req.body:

let reportAnalytics = [];

As we did before, we need to check if the storeAnalytics.json file exists. If the file exists, we’ll use JSON.parse on reportFile to convert the file content to a JavaScript object:

if (fs.existsSync(`${__dirname}/storeAnalytics.json`)) {
const reportFile = await fsp.readFile(`${__dirname}/storeAnalytics.json`, ‘utf-8’)
reportAnalytics = JSON.parse(reportFile)
} else {
return (‘File does not exist’);
}

Now, we can save the IP address and coordinates of the user in the storeAnalytics.json file. Anytime the user requests to calculate the geolocation based on the coordinates provided, the IP address will be included in the request in form of a query.

Now that we’ve gotten the IP address from the req.query object, we can write the code to check if the IP address provided in the req.query object is the same as the IP address stored in the storeAnalytics.json file:

for (let i=0; i<reportAnalytics.length; i++) {
if (reportAnalytics[i].ip !== ip) {
return (‘No Coordinates found with that IP’);
};
}

In the code above, we are using forloop to loop through the reportAnalytics array. We initialized the variable i, which represents the current element’s index in the reportAnalytics array, to 0. If i is less than the length of the reportAnalytics array, we increment it.

Next, we check if the reportAnalytics array’s IP address property equals the IP address provided in the req.query.

Let’s calculate the location for IP addresses stored only in the last hour:

const hourAgo = new Date();
hourAgo.setHours(hourAgo.getHours()-1);
const getReport = reportAnalytics.filter(el =>
el.ip === ip && new Date(el.createdAt) > hourAgo
)

In the code block above, we created a constant called hourAgo and set it to a Date object. We used the setHours() method to set hourAgo to the last hour getHours()-1.

When the current IP addresses in the reportAnalytics file are equivalent or equal to the IP addresses passed in the req.query, meaning the data was created in the last hour, getReport creates a constant set to a new array.

Create a constant called coordinatesArray, which will store only the coordinates that have been saved in the getReport array:

const coordinatesArray = getReport.map(element => element.coordinates)

Next, we need to calculate the location with the coordinates. We need to iterate through the coordinatesArray and calculate the location by passing in the two values saved as the coordinates:

let totalLength = 0;
for (let i=0; i<coordinatesArray.length; i++) {
if (i == coordinatesArray.length – 1) {
break;
}
let distance = calculateDistance(coordinatesArray[i], coordina tesArray[i+1]);
totalLength += distance;
}

In the code above, totalLength represents the total distance calculated from the two coordinates. To iterate through thecoordinatesArray, we need to initialize the result of our calculation. Setting totalLength to zero initializes the total distance.

The second line contains the iteration code we are using forloop. We initialize the i variable with let i=0. The i variable represents the index of the current element in the coordinatesArray.

i<coordinatesArray.length sets the condition of the iteration to run only when the index of the current element is less than the length of the coordinatesArray. Next, we increment the index of the current element in the iteration to move to the next element with i++.

Next, we’ll check if the index of the current element is equal to the number of the last element in the array. Then, we pause the iteration code execution and move to the next one using the break keyword.

Finally, we create a function called calculateDistance that accepts two arguments, the first and second coordinate values (longitude and latitude). We’ll create calculateDistance in another module and export it to the fetchController.js file, then we’ll save our final result in the totalLength variable that we initialized.

Note that every request needs a response. We’ll respond with a statusCode of 200 and a JSON containing the value of the distance we will calculate. The response will only be shown if the code is successful:

res.status(200).json({distance: totalLength})

Your fetchController.js file should look like the following two code blocks:

The fetchController.js file
fetchController.js file continuation

Build the calculateDistance function

In your working directory, create a new folder called utilities and create a file called calculateDistance.js inside. Open the calculateDistance.js file and add the following function:

const calculateDistance = (coordinate1, coordinate2) => {
const distance = Math.sqrt(Math.pow(Number(coordinate1.x) – Number(coordinate2.x), 2) + Math.pow(Number(coordinate1.y) – Number(coordinate2.y), 2));
return distance;
}
module.exports = calculateDistance;

In the first line, we create a function called calculateDistance that accepts two arguments: coordinate1 and coordinate2. It uses the following equations:

Math.sqrt: square root in math

Math.pow: raises a number to a power

Number(): converts a value to a number

coordinate1.x: the second value of the first coordinate (longitude)

coordinate2.x: the first value of the first coordinate (longitude)

coordinate1.y: the second value of the second coordinate (latitude)

coordinate2.y: the first value of the second coordinate (latitude)

Now that we have created the calculateDistance function, we need to require the function into our code in the fetchController.js file. Add the code below after the fs module:

const calculateDistance = require(‘../utilities/calculateDistance’);

Implementing error handling

It’s important to implement error handling in case our code fails or a particular implementation doesn’t work the way it was designed to. We’ll add handling for errors both in development and production.

Open your config.env file and run NODE_ENV=development to set the environment to development.

In your controllers folder, create a new file called errorController.js. The code snippet below creates a function called sendErrorDev to handle errors encountered in the development environment:

const sendErrorDev = (err, res) => {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack,
});
}

We’ll create a function called sendErrorDev that accepts two parameters, err for the error and res for the response. The response.status takes in the statusCode of the error and responds with JSON data.

Additionally, we’ll create a function called sendErrorProd that will handle errors encountered when the API is in the production environment:

const sendErrorProd = (err, res) => {
if(err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
console.error(‘Error’, err);
res.status(500).json({
status: ‘error’,
message: ‘Something went wrong’
})
}
}

In your utilities folder, create a file called appError.js and type the following code:

class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith(‘4’) ? ‘fail’ : ‘error’;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;

We’ll create a class called AppError that extends the Error object.

Then, we’ll create a constructor that will initialize the object of the class. It accepts two parameters called message and statusCode. The super method calls the constructor with one argument, passes it into message, and gains access to the constructor’s properties and methods.

Next, we’ll set the statusCode property of the constructor to statusCode. We set the status property of the constructor to any statusCode that begins with 4, for example, the 404 statusCode to fail or error.

Create another file called catchAsync.js and add the following code in it:

module.exports = fn => {
return (req, res, next) => {
fn(req, res, next).catch(next);
}
}

Add error handling to the controller files

Require the appError.js file and the catchAsync.js file in your storeController.js and fetchController.js files. Place these two import statements at the top of your code in both files:

const catchAsync = require(‘../utilities/catchAsync’);
const AppError = require(‘../utilities/appError’);

In the storeController.js and fetchController.js files, wrap your functions with the catchAsync() method as follows:

// For storeController.js file
exports.postAnalytics = catchAsync(async(req, res, next) => {…}

// For fetchController.js file
exports.getAnalytics = catchAsync(async(req, res, next) => {…}

Next, in your fetchController.js file, run the AppError class:

for (let i=0; i<reportAnalytics.length; i++) {
if (reportAnalytics[i].ip !== ip) {
return next(new AppError(‘No Coordinates found with that IP’, 404));
};
}

Next, run the AppError class in your storeController.js file:

if (fs.existsSync(`${__dirname}/storeAnalytics.json`)) {
const reportFile = await fsp.readFile(`${__dirname}/storeAnalytics.json`, ‘utf-8’)
reportAnalytics = JSON.parse(reportFile)
} else {
return next(new AppError(‘File does not exist’, 404));
}

The code in your storeController.js and fetchController.js files should look like the following screenshots:

Screenshot of the storeController.js file
fetchController.js file lines 1-32
fetchController.js file lines 33-37

Setting up validation

We need to validate that the data received in the req.body, which includes the IP addresses and coordinates, is correct and in the right format. The coordinates should have a minimum of two values, representing longitude and latitude.

In the utilities folder, create a new folder called Validation. In the Validation folder, create a file called schema.js. The schema.js file will contain the desired format for any data provided in the req.body. We’ll use the joi validator:

npm install joi

Type the following code in the schema.js file:

const Joi = require(‘joi’);
const schema = Joi.object().keys({
ip: Joi.string().ip().required(),
coordinates: Joi.object({
x: Joi.number().required(),
y: Joi.number().required()
}).required()
})
module.exports = schema;

In the code block above, we require the joi validator and used it to create our schema. Then, we set the IP address to always be a string and validated the IP address by requiring it in the request body.

We set coordinates as an object. We set both the x and y values, which represent the longitude and latitude values, to be a number and require them for our code to run. Finally, we exported the schema.

In the validation folder, create another file called validateIP.js. Inside, we’ll write code to validate the IP address using the is-ip npm package. Let’s export the package into our code.

In the validateIP.js file, add the following code:

const isIp = require(‘is-ip’);
const fsp = require(‘fs’).promises;
const fs = require(‘fs’);
exports.validateIP = (req, res, next) => {
if(isIp(req.query.ip) !== true) {
return res.status(404).json({
status: ‘fail’,
data: {
message: ‘Invalid IP, not found.’
}
})
}
next();
}

Run the following command to install the necessary dependencies for our API:

npm install body-parser cors dotenv express fs is-ip joi morgan ndb nodemon

Your app.js file should look like the screenshot below:

app.js file

Under the scripts section in your package.json file, add the following code snippet:

“start:dev”: “node server.js”,
“debug”: “ndb server.js”

Your package.json file should look like the screenshot below:

package.json file

Update your analyticsRoute.js file with the following code:

const express = require(‘express’);
const app = require(‘../app’);
const router = express.Router();
const validateIP = require(‘../utilities/Validation/validateIP’);
const storeController = require(‘../controllers/storeController’);
const fetchController = require(‘../controllers/fetchController’);
router.route(‘/analytics’).post(storeController.postAnalytics).get(validateIP.validateIP, fetchController.getAnalytics);
module.exports = router;

Now, we’ve finished building our location analytics API! Now, let’s test our code to make sure it works.

Testing the API

We’ll use Postman for testing our API. Let’s start our API to ensure it’s running in our terminal:

node server.js

You’ll see the following output in your terminal:

Terminal

The final output of our API, which is hosted on Heroku, should look like the output below:

You can test this API yourself on the hosted documentation.

Conclusion

Location analytics are a great tool for businesses. Location information can allow companies to better serve both prospective and current customers.

In this tutorial, we learned to build a tool that takes location information in the form of IP addresses and coordinates and calculates distance. We set up our file structure in Node.js, built routes to handle GET and POST requests, added error handling, and finally tested our application.

You can use the information you learned in this tutorial to set up your own location reporting API, which you can customize for your own business needs.

The post Build a location analytics reporting API in Node.js appeared first on LogRocket Blog.

Leave a Reply

Your email address will not be published. Required fields are marked *

Send