Combining hapi and AWS Lambda Functions

The serverless computing trend has the potential to drastically change the way web applications are structured and deployed - even if "serverless" is a misnomer. Serverless computing is most commonly associated with Amazon's Lambda offering. Lambda is great because it allows you to treat individual functions as the unit of deployment, while AWS automatically handles scaling, and you're only billed for the compute time that you actually use.

API Gateway to Hell

Lambda's biggest shortcoming is accessing it from the rest of your application, especially if your application is outside of AWS. It would be great to use AWS API Gateway to create a set of HTTP routes powered by Lambda function backends. Unfortunately, the fact of the matter is that, currently, API Gateway is unpleasant, at best, to work with. It requires a lot of manual configuration, is fairly limited when it comes to features, and relies on an unintuitive template mapping system.

A hapier Gateway

If you are willing to take back responsibility over your API server, it is still possible to deploy your route handlers as Lambda functions. This means that you are no longer truly "serverless" but can take full advantage of Lambda's strong points. We created the hapi-gateway module to facilitate this architectural tradeoff.

As the name implies, hapi-gateway is a plugin built on top of hapi, the most out-of-the-box production ready server framework available to Node.js developers. hapi allows developers to define custom route handler types which can take advantage of all of hapi's built in features such as authentication. hapi-gateway registers a new lambda handler type that ties a route to a Lambda function.

For example, assume that you have a Lambda function named 'foo' deployed on AWS. You could access it using hapi-gateway as shown in the following example.

'use strict';

const Hapi = require('hapi');  
const Gateway = require('hapi-gateway');  
const server = new Hapi.Server();

server.connection();  
server.register([  
  {
    register: Gateway,
    // These are options that are applied to all lambda handlers
    options: {
      role: 'arn:aws:iam::XXXX:role/lambda_basic_execution',
      config: {
        accessKeyId: 'YOUR_ACCESS_KEY',
        secretAccessKey: 'YOUR_SECRET_KEY',
        region: 'us-east-1'
      }
    }
  }
], (err) => {
  if (err) {
    throw err;
  }

  server.route([
    {
      // This is a normal hapi route.
      method: 'GET',
      path: '/typical',
      handler (request, reply) {
        reply('a typical hapi route');
      }
    },
    {
      // This calls a lambda function that is already deployed as "foo".
      // If you haven't deployed this already, the route will return a 500.
      method: 'GET',
      path: '/foo-lambda',
      config: {
        handler: {
          lambda: {
            name: 'foo'
          }
        }
      }
    }
  ]);

  server.start((err) => {
    if (err) {
      throw err;
    }

    console.log(`Server started at ${server.info.uri}`);
  });
});

First, add your AWS account access key, secret key, and a role that has permission to execute your Lambda function to the plugin configuration. These credentials are used to invoke the Lambda function. When you start this server, it will print a message to the console, telling you the address it is listening on. Whatever that URL is, add /foo-lambda to the end and navigate to it in a browser. This will invoke your 'foo' Lambda function and return the results. If you haven't deployed a 'foo' Lambda, you'll get a 500 response. It's also worth noting that lambda handlers can peacefully coexist with any other hapi server constructs, as shown in this example by the GET /typical route.

Lambda Input and Output

Lambda functions are defined using the following pattern:

module.exports.handler = function (event, context, callback) {};  

The event parameter is used to pass data to the Lambda function. hapi-gateway passes much of the hapi request object to the Lambda function. This includes the payload, query parameters, path parameters, request headers, cookies, authentication information, and more. The context parameter is an object containing AWS runtime information. hapi-gateway has no control over the context. The final parameter, callback(error, result), is an error first callback that sends back either an error or success result.

In my opinion, the most frustrating aspects of working with AWS API Gateway are the conversion from HTTP request to Lambda input and the conversion from Lambda output to HTTP response. In order to make these interfaces as flexible as possible, hapi-gateway allows you to define them using custom setup() and complete() functions. The output of setup() becomes the event parameter of the Lambda function, while complete() turns the Lambda response into an HTTP response from hapi. For more information on these functions, please see the documentation.

Managing Lambda Functions

At this point, you should be able to run a hapi server that invokes Lambda functions. However, if your application contains more than a couple Lambda functions, you probably don't want to spend the time deploying them all by hand. Luckily, hapi-gateway has you covered.

lambda routes support a deploy option that allows you to specify the filename and exported function that implements your Lambda function. When the hapi server starts up, each Lambda function is bundled and deployed to AWS (overwriting any existing function of the same name) using the lambundaler tool. hapi-gateway also lets you destroy any deployed Lambda functions when the hapi server shuts down, via the teardown option. An example that deploys and tears down code is shown below.

'use strict';

const Path = require('path');  
const Hapi = require('hapi');  
const Gateway = require('hapi-gateway');  
const server = new Hapi.Server();

server.connection();  
server.register([  
  {
    register: Gateway,
    // These are options that are applied to all lambda handlers
    options: {
      role: 'arn:aws:iam::XXXX:role/lambda_basic_execution',
      config: {
        accessKeyId: 'YOUR_ACCESS_KEY',
        secretAccessKey: 'YOUR_SECRET_KEY',
        region: 'us-east-1'
      }
    }
  }
], (err) => {
  if (err) {
    throw err;
  }

  server.route([
    {
      method: 'GET',
      path: '/foo-lambda',
      config: {
        handler: {
          lambda: {
            name: 'foo',
            deploy: {
              source: Path.join(__dirname, 'foo.js'),
              export: 'handler',
              teardown: true
            }
          }
        }
      }
    }
  ]);

  server.start((err) => {
    if (err) {
      throw err;
    }

    // Handle Control+C so the server can be stopped and lambdas torn down
    process.on('SIGINT', () => {
      console.log('Shutting down server...');
      server.stop((err) => {
        if (err) {
          throw err;
        }

        process.exit(0);
      });
    });

    console.log(`Server started at ${server.info.uri}`);
  });
});

This example deploys the function 'handler' in the file 'foo.js' as a Lambda function. Note the 'SIGINT' handler that is used to gracefully shutdown the server. This allows the Lambda function to be torn down. The contents of 'foo.js' are shown below.

'use strict';

module.exports.handler = function handler (event, context, callback) {  
  callback(null, 'hello world!');
};

This example assumes that 'foo.js' is in the same directory as the hapi server. However, because hapi-gateway only requires a file path and an exported function name, you could easily publish each Lambda function as a separate module on npm. The module would only need to export __filename and the handler function's name. You could then require() the module and pass the file and function names to hapi-gateway.

Testing

Because hapi-gateway allows Lambda functions to exist in the same codebase as your API server, it only makes sense that they can be tested at the same time. This means that everything should work without access to AWS. With hapi-gateway, this is as simple as passing a local option to the plugin or individual routes. While this is not a perfect emulation of AWS (yet), it does provide a reasonable estimate. Of course, local execution only works on lambda routes that include their code via deploy.

Conclusion

hapi-gateway aims to make Lambda extremely accessible from hapi, and improve the developer experience over what is currently offered by API Gateway. hapi-gateway is currently not at v1.0.0 because we are interested in receiving feedback, potentially adding new features, and ironing out any bugs before releasing v1.0.0. We'd love to know what you think, and welcome issues and pull requests.