Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Convert A Node.js RESTful API To Serverless With OpenWhisk

TwitterFacebookRedditLinkedInHacker News

When it comes to serverless solutions, there are many options available. If you’re a fan of the Microsoft cloud, you could create Azure Functions. If you’ve been keeping up, I wrote a tutorial called, Take a Node.js with Express API Serverless Using AWS Lambda, which used the Amazon Web Services cloud. Another solution is Apache OpenWhisk, a solution available on IBM’s Bluemix cloud.

We’re going to see how to convert the Node.js with Express application that I had written about in a previous article, and make it serverless with OpenWhisk.

For context, the application that we’ll use in this example will take an image provided by the user, and manipulate it to be compliant Android launcher images. This means we’ll be generating several different sized images, creating a ZIP archive, and returning the ZIP archive to the client. This example is great for serverless because it needs to be able to scale on demand and remain affordable.

The code and how it works can be seen in the article titled, Create an Android Launcher Icon Generator RESTful API with Node.js, and Jimp. If you’d prefer not to go serverless, the same example can be seen as a Docker container in a previous article that I wrote.

Getting Set Up with IBM Bluemix and OpenWhisk

OpenWhisk is an Apache Foundation project. It can be used with or without IBM Bluemix. However, for convenience and IBM’s free tier, we’re going to go the Bluemix route.

Create an account for the IBM Cloud so we can configure the IBM Cloud Functions (OpenWhisk).

In the AWS example I had written about, I used Serverless Framework to do all the heavy lifting. It was a bit buggy for IBM Cloud Functions, so I decided to use the tooling that IBM offered. Download the IBM Cloud Functions CLI for OpenWhisk.

Per the official documentation, execute the following:

wsk property set --apihost openwhisk.ng.bluemix.net

You’ll need to use the --apihost that IBM has provided you when downloading the CLI. You’ll also want to execute the following command to sign into Bluemix:

wsk bluemix login

At this point our CLI is ready for deployment, so we need to create our project.

Create a New Node.js Project for Serverless Functions

Somewhere on your computer, execute the following to create a new Node.js project:

npm init -y

The above command will create a new package.json file. IBM Bluemix doesn’t have any particular Node.js dependencies, but we will need the dependencies found in our previously created project. From the command line, execute the following:

npm install bluebird node-zip jimp --save

As far as the project goes, we’re going to separate our logic from our functions. The functions will exist in a handler.js file and the logic will exist in its own class. In this example, the image processing logic will exist in an image.js file. Execute the following commands:

touch handler.js
touch image.js

If you don’t have the touch command, create the files manually. At this point in time we can start developing our serverless application.

Creating a Function for Image Processing with Jimp for Node.js

We’re going to start by developing our logic since we have most of the work already completed via the previous example. Open the project’s image.js file and include the following JavaScript:

const Zip = new require('node-zip')();
const Bluebird = require("bluebird");
const Jimp = require("jimp");

Jimp.prototype.getBufferAsync = Bluebird.promisify(Jimp.prototype.getBuffer);

class Image {

    constructor(url) {
        this.url = url;
    }

    generate(callback) {
        return new Promise((resolve, reject) => {
            Jimp.read(this.url, (error, image) => {
                if(error) {
                    reject(error);
                }
                var images = [];
                images.push(image.resize(196, 196).getBufferAsync(Jimp.AUTO).then(result => {
                    return new Bluebird((resolve, reject) => {
                        resolve({
                            size: "xxxhdpi",
                            data: result
                        });
                    });
                }));
                images.push(image.resize(144, 144).getBufferAsync(Jimp.AUTO).then(result => {
                    return new Bluebird((resolve, reject) => {
                        resolve({
                            size: "xxhdpi",
                            data: result
                        });
                    });
                }));
                images.push(image.resize(96, 96).getBufferAsync(Jimp.AUTO).then(result => {
                    return new Bluebird((resolve, reject) => {
                        resolve({
                            size: "xhdpi",
                            data: result
                        });
                    });
                }));
                images.push(image.resize(72, 72).getBufferAsync(Jimp.AUTO).then(result => {
                    return new Bluebird((resolve, reject) => {
                        resolve({
                            size: "hdpi",
                            data: result
                        });
                    });
                }));
                images.push(image.resize(48, 48).getBufferAsync(Jimp.AUTO).then(result => {
                    return new Bluebird((resolve, reject) => {
                        resolve({
                            size: "mdpi",
                            data: result
                        });
                    });
                }));
                Bluebird.all(images).then(data => {
                    for(var i = 0; i < data.length; i++) {
                        Zip.file(data[i].size + "/icon.png", data[i].data);
                    }
                    var d = Zip.generate({ base64: true, compression: "DEFLATE" });
                    var response = {
                        headers: {
                            "Content-Type": "application/zip",
                            "Content-Disposition": "attachment; filename=android.zip"
                        },
                        body: d
                    };
                    resolve(response);
                });
            });
        });
    }

}

module.exports = Image;

Let’s briefly discuss what is happening in the above code and how it is different than what we saw in the Node.js with Express Framework version.

We already know our goal is to manipulate images and create a ZIP archive of them, hence the jimp and node-zip packages that we imported. For controlling and keeping track of our asynchronous pieces, we had to convert our callbacks to promises with the bluebird package. There are other ways to force synchronous operations in Node.js as seen in a previous article that I wrote titled, Waiting for a Loop of Async Functions to Finish in Node.js.

First we read the file buffer and do a few resize operations on the image. When the resize operations are finished, we end up in the following chunk of JavaScript code:

Bluebird.all(images).then(data => {
    for(var i = 0; i < data.length; i++) {
        Zip.file(data[i].size + "/icon.png", data[i].data);
    }
    var d = Zip.generate({ base64: true, compression: "DEFLATE" });
    var response = {
        headers: {
            "Content-Type": "application/zip",
            "Content-Disposition": "attachment; filename=android.zip"
        },
        body: d
    };
    resolve(response);
});

Each of the transformed buffers are added to a ZIP and the ZIP is created as a base64 encoded string. We then resolve our promise with response headers defined and the base64 encoded string included. OpenWhisk will take the Content-Type header and know how to return the body to the client. In other words, the base64 string will be returned as a binary file.

So what exactly is different in this example with OpenWhisk than our example with Express Framework?

For one, we’re not taking file requests and sending responses using Express. Instead, we’re wrapping the generate function in a promise and resolving it when we have our ZIP archive data. The function itself will handle the promise.

Now let’s take a look at the handler.js file for our functions. Open it and include the following code:

const Image = require("./image");

function main(params) {
    var i = new Image(Buffer.from(params.__ow_body, "base64"));
    return i.generate();
}

exports.main = main;

Pretty slick right?

What we’re doing is taking the request body, which is a base64 encoded string, and converting it into a buffer. This buffer is passed to our Image class and the promise is returned. Even though our request body is interpreted as base64 data, we’re really going to be sending an image file.

So how do we deploy this function to the IBM Cloud?

Deploying the Function and Creating Web Actions for API Access

Two things need to happen when deploying our function. We need to deploy the function and we need to configure a web action so HTTP requests can trigger it.

When it comes to deploying the function, there is something a bit strange here. We can’t just create an action because the Image class won’t be included. In an ideal situation, the deployment would recognize the local dependencies and include them. This is not the case. Instead we must first create a ZIP archive of our project.

zip -r action.zip *

The above command will create a ZIP archive of everything including the node_modules directory. With the ZIP archive available, execute the following:

wsk action create generate --kind nodejs:default action.zip --web raw

The above command will create an action called generate using the action.zip file. By including --web raw we are saying that we want raw bodies to be allowed and processed.

Once the function is created, we can create our web action.

With the OpenWhisk CLI, execute the following:

wsk api create /process POST generate --response-type http

The above command will create an API endpoint at /process that accepts POST requests. The endpoint will trigger the generate function that was previously uploaded. Probably the most important part of this command is the --response-type flag. The response must be http, otherwise custom response headers are not allowed. We need custom response headers to be able to specify that our response is a binary response.

After creating the API endpoint, you should get a URL response that looks similar to the following:

https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/fdb44d34cdd2598302f0697c5d53640bb9c1f75ea2402657e6/process

This URL is how you’ll be able to access your API and function.

Testing the OpenWhisk Function with cURL

There are many ways to test an API. You could use something like Postman, but at the end of the day, cURL is the easiest and most readily available solution.

With the function deployed, execute the following command:

curl -X POST -H "Content-Type: image/png" --data-binary @./icon.png https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/44d34cdd2598302f0697c5d53640bb9c1f75ea2402657e6/process >> android.zip

The above command will issue a POST request with a binary body. The body will be a local file relative to the command line path. The result will be saved to an android.zip file.

Conclusion

You just saw how to move an image processing web API created with Node.js and Express Framework to serverless with OpenWhisk on IBM’s Bluemix cloud platform. The web API used in this example was taken from a previous article that I had written about. Image processing is a great use case for serverless because generally image procesing requires powerful servers. It is expensive to keep powerful servers online all the time. In the serverless scenario, the functions are used as necessary and billed as such which is much cheaper and, in my opinion, easier to operate than managing a server.

If you’re interested in the Amazon cloud rather than IBM, check out a similar tutorial that I had created titled, Take a Node.js with Express API Serverless Using AWS Lambda. If you’d rather keep the Node.js with Express application as is, but make it easier to deploy and maintain, check out my example titled, Containerizing a Node.js with Express Image Processing RESTful API Using Docker.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.