Check out bidbear.io Amazon Advertising for Humans. Now publicly available ๐Ÿš€

AWS Lambda: Boilerplate

Intro

As my understanding and practical usage of AWS Lambda has grown iโ€™ve found some useful lambda patterns. This post is a collection of those methods, which you should be able to pick and combine to format uniform reliable Lambda functions.

Here is a sample function that combines many of these patterns as an example:

sample-lambda.js
// brief description of function objective

const axios = require("axios");
const Sentry = require("@sentry/serverless");

Sentry.AWSLambda.init({
  dsn: "https://YOUR_DSN.ingest.sentry.io/YOUR_DSN",
});

// wrap lambda with sentry for error reporting
exports.handler = Sentry.AWSLambda.wrapHandler(async (event, context) => {
  // log event for debugging
  console.log("๐ŸŒŽ EVENT", event);

  // standardized error handler
  const handleError = (error) => {
    console.error("โš  Full Error Code", JSON.stringify(error));

    const errorResponse = {
      statusCode: 400,
      message: error.message,
      requestId: context.awsRequestId,
      function_name: process.env.AWS_LAMBDA_FUNCTION_NAME,
      function_version: process.env.AWS_LAMBDA_FUNCTION_VERSION,
    };

    console.log("๐Ÿšง Custom Error Response", errorResponse);

    throw new Error(JSON.stringify(errorResponse));
  };

  // wrap function executions in try/catch
  try {
    // if fetching data from multiple sources, or performing multiple database
    // operation, use this promise mapping technique to run them synchronously

    let apiFetchConfigOne = {
      method: "get",
      url: "https://someapi.com/some-endpoint",
      headers: {
        Authorization: someToken,
      },
      data: "",
    };

    let apiFetchConfigTwo = {
      // another config
    };

    let apiFetchConfigThree = {
      // another config
    };

    // in this example it is an array of configs, but it could be an array
    // of anything, such as profile id's, that you want to run synchronous
    // operations on
    let fetchApiConfigs = [
      apiFetchConfigOne,
      apiFetchConfigTwo,
      apiFetchConfigThree,
    ];

    // map whatever array you want to run synchronously
    let fetchApiPromises = fetchApiConfigs.map((config) => {
      return axios(config)
        .then((response) => {
          // if you don't want to return full response use .then to parse desired return
          return response.data;
        })
        .catch((error) => {
          handleError(error);
        });
    });

    // run all operations synchronously
    let allApiResponses = await Promise.all(fetchApiPromises);
    // if you have arrays within arrays at this point you can flatten
    let apiResponses = allApiResponses.flat();

    if (apiResponses) {
      // return data in body with function name and version
      let response = {
        statusCode: 200,
        body: {
          apiResponses,
          function_name: process.env.AWS_LAMBDA_FUNCTION_NAME,
          function_version: process.env.AWS_LAMBDA_FUNCTION_VERSION,
        },
      };
      return response;
    }
    // if we fail to achieve desired result
    else {
      // we can return custom errors
      handleError({ message: "failed to fetch api responses" });
    }
  } 
  // catch uses our standardized error handler
  catch (error) {
    handleError(error);
  }
});

These patterns can be combined with what we learned about in these posts:

๐Ÿ“˜ Ncoughlin: API Gateway Stage Variables

๐Ÿ“˜ Ncoughlin: Passing errors from Lambda to API Gateway

Response Formatting

Remember that our responses format can be completely customized, and in fact should be. We should pass back exactly the information that we need to, and nothing more. This is a security concern, and also a performance concern. We donโ€™t want to be passing back more data than we need to, and we donโ€™t want to be passing back sensitive data. Checks for correct data should be made server side, and errors should be recognized and send 400 responses to the client without passing the full error stacktrace.

We can explore the stack trace in our logs. We can also use Sentry to log errors and explore them in the Sentry dashboard.

The environment variables for function and version are very useful if you are using API Gateway stage variables, to ensure that the correct version of the function is being called.

Success

let response = {
    statusCode: 200,
    body: {
        apiResponses,
        function_name: process.env.AWS_LAMBDA_FUNCTION_NAME,
        function_version: process.env.AWS_LAMBDA_FUNCTION_VERSION,
    },
};
return response;

Error Handling

If we create a standardized error handling function we can call it in our catch blocks to return a consistent error response to the client, or call it conditionally with a custom fail condition.

If we use a consistent error response across all functions, we can also have a consistent mapping template in api gateway to recognize and handle these errors.

// standardized error handler
const handleError = (error) => {
  console.error("โš  Full Error Code", JSON.stringify(error));

  const errorResponse = {
    statusCode: 400,
    message: error.message,
    requestId: context.awsRequestId,
    function_name: process.env.AWS_LAMBDA_FUNCTION_NAME,
    function_version: process.env.AWS_LAMBDA_FUNCTION_VERSION,
  };

  console.log("๐Ÿšง Custom Error Response", errorResponse);

  // must throw error response in actual error for API Gateway to recognize
  // and handle it properly
  throw new Error(JSON.stringify(errorResponse));
};

Calling our error handler will be as simple as:


try {
    if (someCustomCondition) {
        handleError({ message: "failed to achieve x goal" });
    }
} catch (error) {
    handleError(error);
}

API Gateway settings on the endpoint should be the following to handle these returned errors:

1: Method Response:

  • Add 400 https status code.
  • Add header to that status code Access-Control-Allow-Origin.

2: Integration Response:

  • On Method Response 400 add Lambda Error Regex .*"statusCode":400.* content handling Passthrough.
  • On Response Header Access-Control-Allow-Origin add value '*'.
  • Create Mapping Template type application/json with value {"message":$input.json('$.errorMessage')}.

For full explanation of these settings view this post:

๐Ÿ“˜ Ncoughlin: Passing errors from Lambda to API Gateway

Sentry Integration

Full post on integrating Sentry is here:

๐Ÿ“˜ Ncoughlin: Sentry Integration

Make sure the sentry layer is added to your function.

const Sentry = require("@sentry/serverless");

Sentry.AWSLambda.init({
  dsn: "https://YOUR_DSN.ingest.sentry.io/YOUR_DSN",
});

// wrap lambda with sentry for error reporting
exports.handler = Sentry.AWSLambda.wrapHandler(async (event, context) => {

    // function code

});

Common Dependencies

Here are just some common dependencies for me to copy paste into my functions. Iโ€™m sure you have your own.

// aws
const AWS = require("aws-sdk");
const ddb = new AWS.DynamoDB.DocumentClient({ region: "us-east-1" });
const s3 = new AWS.S3({ apiVersion: '2006-03-01' });
const stepfunctions = new AWS.StepFunctions({ apiVersion: '2016-11-23' });
// api calls
const axios = require("axios");
const qs = require("qs");
// error reporting
const Sentry = require("@sentry/serverless");

Configure Timeout

Iโ€™ve run into an issue where my lambda functions were timing out for real users in production and this is infuriating. The default timeout is 3 seconds. Depending on what the function is doing, you will absolutely hit this limit and without a timeout error being thrown. The function will just stop executing and return nothing to the client. This is a huge problem.

Anytime you are performing api or database actions in a lambda function, increase the timeout by a lot.

Promise.all()

If you are performing multiple api or database actions in a lambda function, you can use Promise.all() to run them synchronously. This is a great way reduce the amount of time your function takes to execute, and also to reduce the amount of time you are paying for. Definitely donโ€™t put a database call in a loop, if you donโ€™t know the exact number of times that loop will run.

// if fetching data from multiple sources, or performing multiple database
// operation, use this promise mapping technique to run them synchronously

let apiFetchConfigOne = {
    method: "get",
    url: "https://someapi.com/some-endpoint",
    headers: {
    Authorization: someToken,
    },
    data: "",
};

let apiFetchConfigTwo = {
    // another config
};

let apiFetchConfigThree = {
    // another config
};

// in this example it is an array of configs, but it could be an array
// of anything, such as profile id's, that you want to run synchronous
// operations on
let fetchApiConfigs = [
    apiFetchConfigOne,
    apiFetchConfigTwo,
    apiFetchConfigThree,
];

// map whatever array you want to run synchronously
let fetchApiPromises = fetchApiConfigs.map((config) => {
    return axios(config)
    .then((response) => {
        // if you don't want to return full response use .then to parse desired return
        return response.data;
    })
    .catch((error) => {
        handleError(error);
    });
});

// run all operations synchronously
let allApiResponses = await Promise.all(fetchApiPromises);
// if you have arrays within arrays at this point you can flatten
let apiResponses = allApiResponses.flat();

Amazon Ad Analytics For Humans

Advertising reports automatically saved and displayed beautifully for powerful insights.

bidbear.io
portfolios page sunburst chart