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!
To follow along with this tutorial, you’ll need the following:
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:
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:
In this tutorial, we’ll use Express as a dependency. Install Express by running the command below:
npm install express --save
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:
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:
In your app.js
file,require
Express by running the code below:
const express = require('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;
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}`) });
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;
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.
POST
requestCreate 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 objectres
: stands for the response objectnext
: function is called immediately after the middleware exportspostAnalytics = 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:
GET
requestWe 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:
calculateDistance
functionIn 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 mathMath.pow
: raises a number to a powerNumber()
: converts a value to a numbercoordinate1.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');
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); } }
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:
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:
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:
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.
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:
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.
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.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Build a location analytics reporting API in Node.js"
Very well detailed đź‘Ś