Dumitru Glavan (@doomhz )

AWS Lambda functions are extremely efficient at handling small, focused tasks. If you manage to break your entire application in small services then you don’t have to bother maintaining and scaling virtual instances. Having a couple of functions that run on-demand is a lot cheaper than paying for a EC2 instance that has to run indefinitely.

I’ve built a small project (a Lambda function on NodeJS) as a boilerplate for deploying your future Lambda functions on AWS. The job of this function is to handle contact form submissions and send them by email to the site owner. It uses Amazon SES as an email provider and APIGateway to handle requests. You can find the code on GitHub. Please follow the setup and installation steps from the README file.

Lambda handler

That’s the main file/function where your code goes. It can be invoked directly (from aws-cli, aws-sdk, S3/SNS/SQS… AWS events) or by the APIGateway with extra request data.

function handler ( event, context, callback ) { console .log( 'Lambda params:' , JSON .stringify(event)) if (event.httpMethod) { const route = ` ${event.httpMethod} _ ${data.resource} ` switch (route) { case 'POST_/ContactForm' : ... break default : callback() } } else if (event.task) { switch (event.task) { case 'CONTACT' : ... break default : callback() } } else { callback() } }

The first event parameter holds the data sent along with the request or the direct invocation. The easiest way to differentiate between the two invocation types is to check if there is some APIGateway specific data set on the event. The HTTP method set by the APIGateway makes the difference.

When invoking our Lambda function directly, it’s recommended to set an extra property on the event that holds the task name (i.e. event.task ).

Lambda event data

APIGateway has a pre-defined standard of passing along the data to a Lambda handler. This data is parsed on every invocation. The request body parameters come in as a stringified JSON.

function parseApiGatewayEventData ( event ) { const body = {} try { Object .assign(body, event.body) } catch (e) { console .warn( `Could not parse Lambda body params: ${event.body} ` ) } return { headers : event.headers, path : event.resource, method : event.httpMethod, queryParams : event.queryStringParameters, pathParams : event.pathParameters, bodyParams : body } }

In case of a direct invocation, the data comes in as a JSON object.

Lambda response

APIGateway expects a special response format. Mostly, you’ll need to set headers , a status code and a JSON body response. It’s worth noticing that the response body must be a stringified JSON, otherwise APIGateway will generate an Internal error .

function respondToLambdaRoute ( callback, response, error ) { const statusCode = error ? 500 : 200 if (error) { response = typeof error === Error ? error.message : error } try { response = JSON .stringify(response) } catch (e) { response = ` ${response} ` } const apiGatewayResponse = { statusCode, headers : { 'Access-Control-Allow-Origin' : '*' , 'Access-Control-Allow-Headers' : '*' , 'Access-Control-Allow-Methods' : 'POST, GET, PUT, DELETE, OPTIONS' , 'Content-Type' : 'application/json' }, body : response } callback( null , apiGatewayResponse) }

Responding to a direct invocation is as easy as sending a string back.

function respondToLambdaTask ( callback, response, error ) { try { response = JSON .stringify(response) } catch (e) { response = ` ${response} ` } callback(error, response) }

The response is a stringified JSON most of the times.

Compiling the Lambda code

Unfortunately, AWS Lambda supports only Node version 4.3 at this time. This means that you don’t have all the goodies available in the new Javascript. This can be easily fixed by setting up Babel and compiling the code to “old” Javascript before packaging and uploading to Lambda. Enabling Babel env presets with fast-async and add-module-exports will let you write shiny code that still works on Lambda with Node 4.3.

rm -rf ./build babel src --out-dir build --copy-files cp ./package.json ./build/ cd ./build NODE_ENV=production npm install rm package.json

It’s important to have a script that uploads the packaged code to Lambda, otherwise you’ll spend a lot of time managing the deployment instead of writing code.

FUNCTION_NAME= "ContactForm" LAMBDA_CODE_PATH=./build ZIP_CODE_FILE= $FUNCTION_NAME .zip npm install npm run build cd $LAMBDA_CODE_PATH rm $ZIP_CODE_FILE zip -r -D $ZIP_CODE_FILE * aws lambda update-function-code \ --function-name $FUNCTION_NAME \ --zip-file fileb:// $ZIP_CODE_FILE

For this script to run, aws-cli must be installed globally on your OS.

Dependencies

babel-polyfill goes as a production dependency, otherwise new JS features like async/await or import/export won’t work. aws-sdk can be installed as a dev dependency only, since the module and the access keys are globally available inside the Lambda function. This will free up some space on Lambda since the code base limit is set to 50MB .

{ ... "dependencies": { "babel-polyfill": "^6.23.0", "handlebars": "^4.0.6" }, "devDependencies": { "aws-sdk": "^2.23.0", "babel-cli": "^6.23.0", "babel-plugin-add-module-exports": "^0.2.1", "babel-preset-env": "^1.1.8", "babel-register": "^6.23.0", "fast-async": "^6.2.1", "lodash": "^4.17.4", "mocha": "^3.2.0", "should": "^11.2.0", "sinon": "^1.17.7" } ... }

The rest of the Babel dependencies are not needed in production.

The end

Hopefully, this small tutorial helps you master the Lambda power a lot faster. Please read the official docs on setting up Lambda and APIGateway before playing with functions. More info on how to setup SES can be found here. Check out the full example on GitHub.