Building a Serverless API Proxy with OpenWhisk

One of the more common tasks we do with a server-side application is to build a simple API proxy. By that I mean we expose an API on our server that simply proxies to another remote server. Why might you do that when you can easily call APIs client-side with JavaScript?

  • The remote API may require a key. Including your key in your JavaScript code means it is exposed to the public and can be used by others, potentially locking you out of the API or running up your charges.
  • The remote API may return data in a undeseriable format, like XML.
  • The remote API may return data you don't need, resulting in a slower response that includes data you won't ever use.
  • The remote API may work with data that doesn't need updating often. Your server can then add it's own caching layer.
  • The remote API may go down. By having your own entry point, you could handle this in multiple ways, with either static data or some other result that won't completely break the clients.
  • Finally, the remote API may get bought by some large company (perhaps one that rhymes with MaceLook) and be shut down. You could replace the remote API completely and your clients will never know.

For all these reasons and more, it may make sense to build a simple server for your web apps and point your code to your API proxy instead of directly at the remote server. While this would be rather trivial with Node.js, I thought it might be a great use case for serverless as well, especially with me just discovering how awesome OpenWhisk is. (See my post from last year: "Going Serverless with OpenWhisk")

For my API, I decided to use the Cat API. The Cat API returns random pictures of cats. It lets you filter by categories, vote on cats, and more. While it doesn't require an API key, but if you don't pass one, you are limited to a set of only 1000 cats, which as we all know is far too few. Also, it returns XML. Which is gross, but I'll forgive them anyway since we're still talking about cats here.

I'm going to assume you read my earlier post, but just in case you didn't, here is a quick overview of how OpenWhisk works. You download a command line program (technically this is optional), write your action (a program that does one thing), and then deploy it to OpenWhisk. The final step is to expose the action via a REST API. Nice and simple, right?

For my testing, I decided to build two actions:

  • The first action will return a list of 10 random cats. It will return image URLs in a small format approrpiate for a list.
  • The second action will return details for one image and ask for a larger image URL.

Let's start with the first action. I began by simply figuring out the URL:

http://thecatapi.com/api/images/get?format=xml&results_per_page=10&size=small&api_key=SECRET

Now I began working on my action. One of the first issues I ran into was how I'd handle the XML. While I could try to parse it by hand, I really wanted to use a npm package, but I also knew that OpenWhisk only supports a particular list of npm packages. I checked that list and was happy to see that xml2js was included! Here's the action I wrote:


var request = require('request');
var parseString = require('xml2js').parseString;

function main() {

    var getCatsURL = 'http://thecatapi.com/api/images/get?format=xml&results_per_page=10&size=small&api_key=visitmywishlist';

    return new Promise((resolve, reject) => {

        request.get(getCatsURL, function(error, response, body) {

            if(error) {
                reject(error);
            } else {

                //lets fix the response
                parseString(body, function(err, result) {
                    if(err) {
                        reject(err);
                    } else {
                        let myResult = result.response.data[0].images[0].image;
                        /*
                        At this point, we have a nice array of results, but each result
                        needs a small change. They look like this:

                         {"id": ["d8p" ],
                         "source_url":["http://thecatapi.com/?id=d8p"],
                         "url":["http://25.media.tumblr.com/tumblr_m4g7z0X8fq1qhwmnpo1_250.jpg"]},                                                                       Notice how the key values are 1-length arrays   
                        
                        */
                        let newResult = myResult.map((cat) => {
                            return {
                                id:cat.id[0],
                                source_url:cat.source_url[0],
                                url:cat.url[0]
                            };
                        });
                        resolve({response:newResult});
                    }
                });
            }

        });


    });

}

Let's tackle this bit by bit. I begin by simply requesting the URL that returns my list of cats. Then I pass it to the parseString method of the xml2js package. This will convert the XML into a simple object, but it does so in a pretty verbose manner. To figure out what I needed, I initially just returned the result of the parse itself. I looked at the result in the CLI and slowly trimmed it down to the bare essentials of what I needed. As a reminder, you can invoke actions from the CLI very easily:

wsk action invoke catlist --blocking

I also have the massage the results a bit since it was returning key/value pairs as 1-length arrays. Here's an example of the output I now get:

Output

To be clear, you're seeing the complete result of the action (well, as much as I could fit in the screenshot), which includes metadata about the action call and the result itself. What will be used by our clients though is within the response.result section.

The next action handles getting details for a particular ID. Here's that action:


var request = require('request');
var parseString = require('xml2js').parseString;

function main(params) {

    var getCatsURL = 'http://thecatapi.com/api/images/get?format=xml&image_id='+params.id+'&size=large&api_key=mymilkshake';

    return new Promise((resolve, reject) => {

        request.get(getCatsURL, function(error, response, body) {

            if(error) {
                reject(error);
            } else {

                //lets fix the response
                parseString(body, function(err, result) {
                    if(err) {
                        reject(err);
                    } else {
                        let myResult = result.response.data[0].images[0].image[0];
                        let detail = {url:myResult.url[0], source_url:myResult.source_url[0], id:myResult.id[0]};
                        resolve({response:detail});
                    }
                });
            }

        });


    });

}

This one is even simpler as we just need to massage the result of one item. Note how I access an expected parameter for the ID: params.id. And honestly, that's it. One issue you may see is that I've got one API key in two separate files. I could correct this by switching my code into a package (see the docs), but that felt like overkill for this particular demo. It would definitely be the right path to take though if I started added more actions related to this one particular API.

The last step was to expose this via a REST API. As a reminder, this is still a work in progress, so you probably don't want to do this yet for real code, but it works great already. The general process with the CLI has you define a root API path, a particular path for this call, the HTTP method, and finally the action to call.

I've got two actions and therefore two API calls. I used a root path of /cats and defined /list and /detail for each part of my API. I showed the CLI for this in my last post, but here it is again:

wsk api-experimental create /cats /list get catwrapper

If you forget what APIs you defined, you can see them via the CLI as well:

wsk api-experimental list

Once I had my APIs defined, I was technically done. For the heck of it, I built an Ionic application. It's just a master/detail type thing, but here it is in action. First, the list:

List

And the detail - that button loads the detail page on the Cat API site.

Detail

The complete code for this application may be found here (https://github.com/cfjedimaster/Cordova-Examples/tree/master/ionic_openwhisk), but let's look at the provider:


import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

/*
  Generated class for the CatProvider provider.

  See https://angular.io/docs/ts/latest/guide/dependency-injection.html
  for more info on providers and Angular 2 DI.
*/
@Injectable()
export class CatProvider {

  private LIST_URL = 'https://3b1fd5b1-e8cc-4871-a7d8-cc599e3ef852-gws.api-gw.mybluemix.net/cats/list';

  private DETAIL_URL = 'https://3b1fd5b1-e8cc-4871-a7d8-cc599e3ef852-gws.api-gw.mybluemix.net/cats/detail';

  constructor(public http: Http) {
    console.log('Hello CatProvider Provider');
  }

  list() {

    return this.http.get(this.LIST_URL)
    .map(res => res.json())
    .map(data => data.response);

  }

  get(id) {
    return this.http.get(this.DETAIL_URL + '?id='+id)
    .map(res => res.json())
    .map(data => data.response);
  }

}

So yeah, nothing special here, nothing unusual, which is exactly what we want! Our Ionic app speaks to our OpenWhisk implementation and the actual API is completely hidden from the user. (Err, ok technically not - since the detail page opens up on the site, but you get the point.) There's no API key. No XML parsing. That's all hidden behind the scenes. And even better, I didn't have to write or setup my own NodeJS server. All I did was write two small actions and deploy them and I was done.

If you can't tell yet - I freaking love this!

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

Want to read more like this?