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:
// 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 handlingPassthrough
. - 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();
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.