Hosting a Serverless Hapi.js API with AWS Lambda

Ancient Knowledge

This article is getting old. It was written in the ancient times and the world of software development has changed a lot since then. I'm keeping it here for historical purposes, but I recommend you check out the newer articles on my site.

CAUTION: This article is for Hapi versions older than 17.x. If you are using a newer version of Hapi, please checkout the new setup here.

A good friend of mine wrote an excellent demo of how to use the Serverless framework to build an AWS Lambda function that can serve more than one request. The folks over at Serverless have named this approach the "Monolithic Pattern" and its a great way to move into serverless hosting without having the sprawl of multiple hosted Lambda functions.

npm version

It also helps avoid the dreaded cold-start problem as well. I won't go into detail about how to use the Serverless framework in this post because I want to focus on the Hapi->Lambda interface itself. There is a full working demo in the link at the bottom that includes everything.

I have been a big fan of the Hapi.js framework for a few years now and really like how it pulls together a complete end-to-end solution for a REST API. It covers request and response validation, authorization, process monitoring, an excellent module/plugin system, and a plugin for everything else. There is also dropin plugins available to generate your swagger.json files so you don't have to write that crud by hand.

AWS Lambda is becoming more popular for hosting functions on the cloud without needing to spin up a traditional VM or mess with Docker images. Typically, you would host a simple function using javascript in the following way:

exports.handler = function(event, context, callback) {
    var response = {
        statusCode: 200,
        body: JSON.stringify({status: "OK"});
    };
    callback(null, response);
};

Using API Gateway, you can then map a route such as GET /hello/world to this function and make HTTP requests to it. This is great for one-off functions that can be easily separated out into there own Lambda function. The drawback to this approach if you have a lot of functions to host is the pain of provisioning this many things in AWS and the risk of cold-start issues. See the patterns article from Serverless on the pros and cons of each approach. Hapi.js on the other hand provides you with a full routing system and an HTTP listener. What we need to do is use only the routing, and ignore the HTTP side of things. Lets take a look at this simple Hello World of Hapi.js

const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({ port: 3000, host: 'localhost' });

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, reply) {
        reply('Hello, world!');
    }
});

server.start((err) => {
    if (err) throw err;
    console.log(`Server running at: ${server.info.uri}`);
});

If you run this locally, Hapi will start listening on port 3000 for any HTTP requests that match the defined routes. To host this in Lambda, we need to take out the listener and invoke the correct function based on the route that Lambda receives. Looking back at that Lambda example, we can see that it passes an event object into the function. This event object contains information about the raw request such as headers, body, path, and parameters. We can use this information and the the inject method of the Hapi server to directly call our API from Lambda.

const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection();

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, reply) {
        reply('Hello, world!');
    }
});

exports.handler = (event, context, callback) => {

    // map lambda event to hapi request
    const options = {
        method: event.httpMethod,
        url: event.path,
        payload: event.body,
        headers: event.headers,
        validate: false
    };

    server.inject(options, function(res){
        const response = {
            statusCode: res.statusCode,
            body: res.result
        };
        callback(null, response);
    });

};

If your using the Hapi plugin system for your development (and you should be), you need to add some extra plumbing to handle the plugin system register callback. A full example can be found at https://github.com/carbonrobot/hapi-lambda-demo or you can use the npm package which has more up to date code that handles headers properly. https://github.com/carbonrobot/hapi-lambda