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.