Locations, defined by latitude and longitude, can be used in combination with other data to generate insights for businesses, known as location analysis.

Businesses operating globally use location analytics throughout the value chain, for example, to target users, provide services, and run targeted advertising. With the rise of social media and mobile devices, the use of location analytics has increased globally.

In this tutorial, you’ll learn how to build a lightweight location analysis reporting service API in Node.js. By the end of this tutorial, you will be able to build this type of API for your own projects. You’ll also get a better understanding of error handling and good file structure in Node.js

Let’s get started!

The premise condition

To continue with this tutorial, you need the following.

  • Familiar with Node.js, Express, and Git
  • Visual Studio code editor
  • Heroku account
  • The Postman account

Setting the file structure

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

mkdir lars

Copy the code

Open the lars working directory in the VS code editor.

  code .

Copy the code

You will 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 want to run this command on a terminal outside of VS Code, navigate to the lars directory and run the following command.

npm init -y

Copy the code

The above code automatically generates the package.json file.

VS Code displays the package.json file created

In this tutorial, we will use Express as a dependency. Install Express by running the following command.

npm install express --save

Copy the code

A command to install Express as a dependency

After installing Express, you’ll notice that a node_modules folder has been created. To confirm that you have Installed Express, check your package.json file and you will see that Express is installed as a dependency.

The node_modules folder is created, and Express is added to package.json.

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

A screenshot of the VS code window shows that app.js has been created.

In your app.js file, run the code requireExpress below.

const express = require('express');

Copy the code

Import Express

Now, call Express to create your application, route, and port on which your application will run.

const app = express();

Copy the code

Node.js is modular, which means it breaks your application into modules, or files, and exports each file. We will export the app using the export keyword.

module.exports = app;

Copy the code

App. Js file

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

const app = require('./app');

Copy the code

Create a file named config.env in the same directory as server.js. Config. Env file to our application needs to include all the [process. Env] (https://nodejs.org/dist/latest-v8.x/docs/api/process.html), we need all of the key applications. In the config.env file, create a PORT variable and set PORT to listen on PORT 8000.

PORT=8000

Copy the code

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

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

Copy the code

Finally, we’ll set up the application to listen on this port with the.listen() method.

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

Copy the code

Build routing

Every time you visit a web page or an application running on the network, you are making an HTTP request. The server responds with data from the background or database, which is called an HTTP response.

When you create a resource on a web application, you are invoking a POST request. Similarly, if you are trying to DELETE or UPDATE a resource on a Web application, you are invoking a DELETE, PATCH, or UPDATE request. Let’s set up a route to handle these requests.

Create a folder called Routes in your working directory and create a file called analyticsroute.js in it. Require is expressed in the analyticsroute.js file to set up the route of the API.

      const express = require('express');

Copy the code

We also need to require our application modules from the app.js file.

const app = require('.. /app');Copy the code

Then, we create our route.

        const router = express.Router();

Copy the code

Finally, we export the router.

        module.exports = router;

Copy the code

Set up controller

We need to create a file for the controller and import it into our analyticsRoutes file. First, create a folder called Controllers in your working directory.

Our API will use the user-supplied IP address and coordinates to calculate distance and location. Our request needs to accept this information as well as requests from the user.

We’ll use a POST request because the user is in req.body. To store this information, we need to require a FS module (file system) in the controller.

To deal withPOSTThe request of

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 returned promise, 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

Copy the code

Type the following code at the top of your file.

const fsp = require('fs').promises;
const fs = require('fs');

Copy the code

Next, we’ll create a controller that handles the routing of our POST requests. We’ll use the exports keyword and create an asynchronous middleware function that takes three arguments.

  • req: indicates the request object
  • res: indicates the response object
  • nextThe: function is called immediately after the middleware output.
postAnalytics = async(req, res, next) => {}
Copy the code

Now we’ll save the properties of the data object in req.Body into the reportAnalytics array. We’ll set up a Date() object to store the creation Date of any data in a createdAt key.

reportAnalytics.push({... req.body, createdAt: new Date()});Copy the code

We’ll create a file called storeAnalytics. Json and, using json.stringify (), save the contents of our reportAnalytics array as a string.

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

Copy the code

When a user requests a POST, we need to check whether the storeAnalytics. Json file exists. If the file exists, we need to read it and save the output.

The output contains a constant named reportFile, which stores the contents of the file being read. Parse converts the contents of the file to a JavaScript object in reportFile using json.parse.

// 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'); }Copy the code

The [fs existsSync ()] (https://www.geeksforgeeks.org/node-js-fs-existssync-method/) method synchronously check file exists. It takes the ${__dirname}/storeAnalytics. Json path as its single argument and points to the location of the file we want to examine.

We await the result of reading the file with the fsp.readFile() method while we await the await keyword with reportFile. Next, we use (${__dirname}/storeAnalytics. Json to specify the path to the file we want to read. We set the encoding format to UTF-8, which converts what is read from the file to a string.

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

If the file is successfully read, created, and saved at storeAnalytics. Json, we need to send a response. We will use the response object (RES), which is the second argument to our asynchronous postAnalytics function.

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

Copy the code

We will respond with a status success and data message IP and Coordinates successfully taken.

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

To deal withGETThe request of

We need to create another controller file to handle our GET request. When a user makes a GET request to the API, we will calculate their location based on their IP address and coordinates.

Create a file called fetchController.js in the controllers folder. Fs In the storeController.js file, we need the require module and the fsPromises. ReadFile () method to handle returned promises.

const fsp = require('fs').promises;
const fs = require('fs');

Copy the code

Let’s create a controller to handle our routing of GET requests. We will use similar middleware functions and parameters to handle the ABOVE POST request.

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

Copy the code

In the getAnalytics middleware, enter the following code to get the IP address from the requested query.

     const { ip } = req.query; 

Copy the code

Now create an empty array to store the contents of req.body.

     let reportAnalytics = [];

Copy the code

As we did earlier, we need to check if the storeAnalytics. Json file exists. If the file exists, we will use json.parse on reportFile to convert the file contents into 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');
    }

Copy the code

Now we can save the user’s IP address and coordinates in the storeAnalytics. Json file. Any time a user requests to compute a geographic location based on the provided coordinates, the IP address will be included in the request as a query.

Now that we have the IP address from the req.query object, we can write code to check that 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'); }; }Copy the code

In the above code, we use forloop to loop through the reportAnalytics array. We initialize the variable I, which represents the index of the current element in the reportAnalytics array, to 0. If I is less than the reportAnalytics array length, we increment it.

Next, we check that the IP address property of the reportAnalytics array is equal to the IP address provided in req.query.

Let’s calculate the location of the 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
    )

Copy the code

In the code block above, we create a constant named hourAgo and set it to a Date object. We use the setHours() method to set hourAgo to getHours()-1 for the last hour.

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

Create a constant called coordinatesArray that will store only coordinates already saved in the getReport array.

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

Copy the code

Next, we need to use coordinates to figure out the position. We need to walk through the coordinatesArray and calculate the position by passing in two values that are saved as 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;
    }

Copy the code

In the above code, totalLength represents the total distance calculated from the two coordinates. To traverse the coordinatesArray, we need to initialize our calculations. Set totalLength to zero to initialize the total distance.

The second line contains the iteration code we used, 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 iteration conditions and only runs if 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, i++.

Next, we check whether the index of the current element is equal to the number of the last element in the array. We then pause the iteration and move to the next one using the break keyword.

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

Note that each request requires a response. We will respond with a statusCode of 200 and a JSON containing the distance value we will calculate. The response is displayed only if the code succeeds.

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

Copy the code

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

FetchController. Js file

This is a continuation of the fetchController.js file

To establishcalculateDistancefunction

In your working directory, create a new folder called Utilities and create a file called calculateDistance.js. Open the calculateDistance.js file and add the following functions.

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;

Copy the code

In the first line, we create a function called calculateDistance that takes two parameters: Coordinate1 and Coordinate2. It uses the following equation.

  • Math.sqrtSquare root in mathematics
  • Math.pow: Raises a number to a power
  • Number(): Converts a value to a number
  • coordinate1.x: 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 that function into our fetchController.js file code. Add the following code after the FS module.

const calculateDistance = require('.. /utilities/calculateDistance');Copy the code

Implement error handling

It is important to implement error handling in case our code fails or a particular implementation doesn’t work the way it was designed. We will add error handling to 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 following code snippet 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,
    });
}

Copy the code

We will create a function called sendErrorDev that takes two arguments, err for error and res for response. Response. status receives an error statusCode and responds with JSON data.

In addition, we will create a function called sendErrorProd that will handle errors encountered by the API in production.

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'
        })
    }
}

Copy the code

In your Utilities folder, create a file called apperor.js and enter 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;

Copy the code

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

We will then create a constructor that will initialize the objects of that class. It takes two arguments, called message and statusCode. The super method calls the constructor with an argument, passes it to Message, and gains access to the constructor’s properties and methods.

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

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

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

Add error handling to the controller file

Require apperor.js and catchAsync.js files in your storeController.js and fetchController.js files. Put the two import statements at the top of the code in both files.

const catchAsync = require('.. /utilities/catchAsync'); const AppError = require('.. /utilities/appError');Copy the code

In the storeController.js and fetchController.js files, wrap your function with the catchAsync() method, as shown below.

// For storeController.js file exports.postAnalytics = catchAsync(async(req, res, next) => {... } // For fetchController.js file exports.getAnalytics = catchAsync(async(req, res, next) => {... }Copy the code

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)); }; }Copy the code

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));
    }

Copy the code

The code in your storeController.js and fetchController.js files should look like the screenshot below.

Screen capture of the storeController.js file

Fetchcontroller.js on lines 1-32

Fetchcontroller.js on lines 33-37

Set the validation

We need to verify that the data in req.body, which includes IP addresses and coordinates, is correct and properly formatted. Coordinates should have at least two values, representing longitude and latitude.

In the Utilities folder, create a new folder named Validation. In the Validation folder, create a file named schema.js. The schema.js file will contain the required format for any data provided in req.body. We will use [joi] validator (https://www.npmjs.com/package/joi).

npm install joi
Copy the code

Enter 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;

Copy the code

Joi in the code block above, we require the validator, which we use to create our schema. We then set the IP address to always be a string and verify the IP address by requesting it in the request body.

We set the coordinates to Object. We set both the x and y values representing the longitude and latitude values to numbers and require them for our code to run. Finally, we export the schema.

In the validator folder, create another file named validateip.js. In it, we will write code to verify the IP address, use [is] – IP NPM package (https://www.npmjs.com/package/is-ip). Let’s export this 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(); }Copy the code

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

Copy the code

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

App. Js file

Add the following code snippet under scripts in your package.json file.

"start:dev": "node server.js",
    "debug": "ndb server.js"

Copy the code

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

Package. The 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;Copy the code

We have now finished building our location analysis API! Now, let’s test our code to make sure it works.

Test the API

We will use Postman to test our API. Let’s launch our API to make sure it runs on our terminal.

node server.js
Copy the code

You should see the following output on your terminal.

terminal

Our API is hosted on Heroku, and its final output should look like the following.

You can test the API yourself in a hosted document.

conclusion

Location analysis is a great tool for businesses. Location information allows companies to better serve potential customers as well as existing customers.

In this tutorial, you learned to build a tool to get location information and calculate distances in the form of IP addresses and coordinates. We set up our file structure in Node.js, set up routes to handle GET and POST requests, added error handling, and finally tested our application.

You can use the information you’ve learned in this tutorial to build your own location reporting API, which you can customize to your business needs.

The postBuild a location analytics reporting API in Node.jsappeared first onLogRocket Blog.