API Builder

API Builder: Geocode Your API Requests

API Builder provides a dashboard for viewing your API Analytics. An example is show below.


The analytics provide useful information, such as which API was called, its response time and which container serviced the API request. However, it would be nice to know where, geographically, the API request came from. This information could be used to determine where to locate servers, for example. Hopefully, this capability will be added to the dashboard in the future.

In the meantime, this blog post describes how to modify your API Builder API in order to collect the geocode coordinates for each API request.

The basic idea is that each API request contains the IP address of the request in the X-Forwarded-For (XFF) header. API Builder can capture this and make a call to an IP-to-Geocode service. In this demo, I use ipinfo.io which has a free tier for the first 1,000 requests per day. We can then store this info in a database which can be queried separately to determine the latitude and longitude of the API requests.

API Builder Block

There are several ways that API Geocoding can be achieved. In this demo, I create a block that can be attached to any model-based API. Note that the code can also be incorporated in a custom API and in a flow-node for API First API development.

The block, getip.js, is shown below:

var Arrow = require('arrow');
var PreBlock = Arrow.Block.extend({
name: 'getip',
description: 'Get IP address of API request',
action: function (req, resp, next) {
if(req.headers['x-forwarded-for']) {
getGoeData(req.headers['x-forwarded-for'], function(e){
var model = Arrow.getModel("apigeoanalytics");
var timeNow = new Date();
var data = {
ip:req.headers['x-forwarded-for'],
method:req.method,
url:req.url,
locData:JSON.parse(e),
createdAt:timeNow
};
model.create(data, function(err, instance){
if(err) {
  console.log('getip: apigeoanalytics record create error, err'+JSON.stringify(err));
  } else {
console.log('getip: apigeoanalytics record created');
}
});
});
}
next();
}
});

function getGoeData(ip, callback) {
var request = require("request");
var options = { method: 'GET',
  url: 'https://ipinfo.io/'+ip+'/geo',
  qs: { token:  },
  headers:
   { 'Cache-Control': 'no-cache' } };
request(options, function (error, response, body) {
  if (error) throw new Error(error);
  if(callback){callback(body);}
});
}
module.exports = PreBlock;

This block needs to be attached to a model. For example, I have an ArrowDB model, employee.js, below. You can see that the block is assigned to the “before” property but could instead be assigned to an “after” property.

var Arrow = require('arrow');
var Model = Arrow.createModel('employee', {
    "connector": "appc.arrowdb",
    "fields": {
        "fname": {
            "type": "string"
        },
        "lname": {
            "type": "string"
        }
    },
    "before": "getip",
    "actions": [
        "create",
        "read",
        "update",
        "delete",
        "deleteAll"
    ]
});
module.exports = Model;

So, whenever an employee API is called, the getip block will be called and will perform the following:

  • Check if the x-forwarded-for header exists.
  • If the header exists, use this IP address and call the ipinfo API to get the latitude and longitude (as well as other info) of the IP address. This is handled by the getGeoData() method.
  • Store the API geodata in a database. In this demo, this is handled by programmatically POSTing to the ArrowDB apigeoanalytics model shown below which stores the ip address, method (GET, POST, …), timestamp of the API request, url of the API and locData which is the data returned by the ipinfo service.
    var Arrow = require('arrow');
    var Model = Arrow.createModel('apigeoanalytics', {
        "description": "Geo Analytics Data for API requests",
        "connector": "appc.arrowdb",
        "fields": {
            "ip": {
                "type": "string",
                "description": "ip address (from API header)"
            },
            "method": {
                "type": "string",
                "description": "REST method (GET, POST, ...)"
            },
            "createdAt": {
                "type": "date",
                "description": "timestamp for API call"
            },
            "url": {
                "type": "string",
                "description": "URL of API"
            },
            "locData": {
                "type": "object",
                "description": "geocode lookup results for IP address"
            }
        },
        "actions": [
            "create",
            "read",
            "update",
            "delete",
            "deleteAll"
        ]
    });
    module.exports = Model;

A sample of the apigeoanalytics data is shown below:

{
  "ip": "119.81.90.78",
  "method": "GET",
  "createdAt": "2018-07-18T16:51:58.270Z",
  "url": "/api/employee",
  "locData": {
    "ip": "119.81.90.78",
    "city": "Badhoevedorp",
    "region": "Noord-Holland",
    "country": "NL",
    "loc": "52.4007,4.9730",
    "postal": "1171"
  }
}

locData above is the data that is returned by the ipinfo service.

Display the API Geodata

Now, we have a database of API Analytics data with geocode information. We can query the data, apigeoanalytics in my example, and get API geodata. I built a simple Titanium mobile app to do this. A sample screen shot is shown below:

The Titanium code to retrieve the API analytics data is shown below:

function getAPIGeoAnalyticsData(since, callback) {
var timeSince = new Date(new Date() -since*60*1000);
var url = Alloy.Globals.baseURL+Alloy.Globals.APIGeoAnalyticsPath+'/query?where={"createdAt":{"$gt":"'+timeSince.toISOString()+'"}}';
var xhr = Ti.Network.createHTTPClient({
    onload: function(e) {
        var reply = JSON.parse(this.responseText);
        if(callback){callback({success:true, data:reply[reply.key]});}
    },
    onerror: function(e) {
        alert('Error retrieving API analytics = '+e.error);
        if(callback){callback({success:false, data:e.error});}
    },
    timeout: Alloy.Globals.httpRequestTimeout
});
xhr.open("GET", url);
xhr.send();
}

In the code above, I perform a query of the analytics data in the last since minutes instead of pulling down all of the analytics data.

Summary

API Geodata is useful to have, so one can determine where API calls are being made. With a small amount of code and database, you can collect this data for your API Builder APIs and then display in a web app or mobile app.