Routing API Gateway Traffic Through One Lamda Function

Routing API Gateway Traffic Through One Lamda Function

One question that I keep hearing is "Can I have more than one handler in my AWS Lambda function?". This usually in the context of someone wanting to implement a services pattern using the API Gateway where all traffic for a resource is handled within a single handler.js.

For example: GET /resource goes to handler.list, POST /resource goes to handler.create, GET /resource/{id} goes to handler.find, etc.

AWS only allows you to define one handler per Lambda function. While you can put multiple handlers into a single handler.js each of these needs to be setup as its own Lambda function.

functions:
  hello:
    handler: handler.createResource
    events:
      - http:
          path: resource
          method: post
          cors: true
    handler: handler.listResources
    events:
      - http:
          path: resource
          method: get
          cors: true
    handler: handler.deleteResource
    events:
      - http:
          path: resource/{id}
          method: delete
          cors: true
    handler: handler.findResource
    events:
      - http:
          path: resource/{id}
          method: get
          cors: true
    handler: handler.updateResource
    events:
      - http:
          path: resource/{id}
          method: patch
          method: put
          cors: true

This solution isn't always ideal.

If you know your events are coming from the API Gateway you can get around this limitation by implementing a handler that routes events internally based on the HTTP method and optional parameters. This allows you to have a single AWS Lambda function that accepts events from the API Gateway instead of one for each HTTP method.

Note: In this example I'm using the API Gateway Lambda proxy integration method. You should be able to do something similar with the older API Gateway Lambda integration method but I'm not going to cover that.

To start I'm going to define a single Lambda function inside my serverless.yml that handles all requests to /resource and /resource/{id}.

functions:
  hello:
    handler: handler.router
    events:
      - http:
          path: resource
          method: any
          cors: true
      - http:
          path: resource/{id}
          method: any
          cors: true

The default response from this handler should be HTTP 405 Invalid HTTP Method. This response tells the client that we don't implement the HTTP method they requested.

module.exports.router = (event, context, callback) => {
  const response = {
    statusCode: 405,
    body: JSON.stringify({
      message: `Invalid HTTP Method: ${httpMethod}`,
    }),
  };

  callback(null, response);
};

Next I'll create two objects that define the handlers to use when interacting with the collection and items in the collection. The collection handlers will be used for requests to /resource and the item handlers for requests to /resource/{id}.

const collectionHandlers = {
  GET: listItems,
  POST: createItem,
};

const itemHandlers = {
  DELETE: deleteItem,
  GET: getItem,
  PATCH: patchItem,
  POST: postItem,
  PUT: putItem,
};

You've probably guessed that the key is the HTTP method so you could define DELETE or PUT on the collection by adding handlers with the relevant keys.

At the top of my handler I need to determine if I'm using the collection handlers or item handlers.This can be done by looking at event["pathParameters"]. AWS sets this value to null when there are no path parameters or an object if there are any path parameters. Because our Lambda function will only be executed for requests to /resource and /resource/{id} I can cheat a little and only check if event["pathParameters"] is null instead of checking if id is set.

let handlers = event["pathParameters"] == null ? collectionHandlers : itemHandlers;

If I wanted to be more cautious I could check that the id path parameter was actually set by using:

let id = event["pathParameters"] !== null && "id" in event["pathParameters"] ? event["pathParameters"]["id"] : undefined;
let handlers = id === undefined ? collectionHandlers : itemHandlers;

Now I need to check if the HTTP method has a handler configured for it. If it does then I can call that method passing the event, context and callback parameters that the main handler received.

let httpMethod = event["httpMethod"];
if (httpMethod in handlers) {
  return handlers[httpMethod](event, context, callback);
}

Remember our default is to return a 405 so we're covered if the handler isn't defined.

Lastly I just need to implement each of the handlers.

The full code for this example is:

"use strict";

const collectionHandlers = {
  GET: listItems,
  POST: createItem,
};

const itemHandlers = {
  DELETE: deleteItem,
  GET: getItem,
  PATCH: patchItem,
  POST: postItem,
  PUT: putItem,
};

module.exports.router = (event, context, callback) => {
  let handlers = event["pathParameters"] == null ? collectionHandlers : itemHandlers;

  let httpMethod = event["httpMethod"];
  if (httpMethod in handlers) {
    return handlers[httpMethod](event, context, callback);
  }

  const response = {
    statusCode: 405,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({
      message: `Invalid HTTP Method: ${httpMethod}`,
    }),
  };

  callback(null, response);
};

function listItems(event, context, callback) {
  const response = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({ action: "listItems" }),
  };

  callback(null, response);
}

function createItem(event, context, callback) {
  const response = {
    statusCode: 201,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({ action: "createItem" }),
  };

  callback(null, response);
}

function deleteItem(event, context, callback) {
  const response = {
    statusCode: 204,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({
      action: "deleteItem",
      id: event["pathParameters"]["id"],
    }),
  };

  callback(null, response);
}

function getItem(event, context, callback) {
  const response = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({
      action: "getItem",
      id: event["pathParameters"]["id"],
    }),
  };

  callback(null, response);
}

function patchItem(event, context, callback) {
  const response = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({
      action: "patchItem",
      id: event["pathParameters"]["id"],
    }),
  };

  callback(null, response);
}

function postItem(event, context, callback) {
  const response = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({
      action: "postItem",
      id: event["pathParameters"]["id"],
    }),
  };

  callback(null, response);
}

function putItem(event, context, callback) {
  const response = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
    },
    body: JSON.stringify({
      action: "putItem",
      id: event["pathParameters"]["id"],
    }),
  };

  callback(null, response);
}

You can also grab this from Github.

Update 12 May 2017: Added the headers and serverless.yml settings to support CORS.