LambdaFlow
A little framework to process data in Lambda Functions connected to API Gateway proxy
(Support APiGateway proxy v1 and v2)
This is a little utility to process data in a Lambda function in a node.js runtime.
A context or "box" will contain the API Proxy event and Lambda function context and will flow through your functions, to be returned as a response at the end.
You will also find some little helpers to help you with error handling and simple HTTP response.
Installation
With npm:
npm install @bjmrq/lambda-flow
With Yarn:
yarn add @bjmrq/lambda-flow
Hello Word Exemple
Javascript:
exports.handler = lambdaFlow(
(box) => {
box.body = "Hello Word";
return box;
}
)();
Typescript:
export const handler = lambdaFlow(
(box) => {
box.body = "Hello Word";
return box;
}
)();
How It Works
Combine Functions
You can create a flow made of multiple functions that will execute one after an other from left to right, similar to a pipe function.
Those function can either be sync or async functions.
exports.handler = lambdaFlow(
(box) => {
console.log(box.event);
return box;
},
async (box) => {
box.body = {
status: "ok"
};
return box;
}
)();
Move Data Around
If you want to move some data from one function to an other you have access to the state key of the box. You can attach any data you like to this state.
exports.handler = lambdaFlow(
(box) => {
const body = JSON.parse(box.event.body);
box.state.parsedBody = body;
return box;
},
async (box) => {
const product = await database("products").where(
"id",
box.state.parsedBody.requestId
);
box.body = product;
return box;
}
)();
Those are the keys accessible inside the box:
- event: is the event object generated by APIGateway
- context: is the context of the Lambda function executing
- callback: is a function that you can call in non-async lambda function handlers to send a response
- state: is a mutable key that you can use to pass data from one function to another
- error: you can attach an error to the error key, doing so will bypass other functions of the flow, only the error handler will be trigger, you can control wether you want to expose this error or not
Control What is Returned
Data on the state will never be return, for this you need to attached data to the body key of the box.
exports.handler = lambdaFlow(
(box) => {
const body = JSON.parse(box.event.body);
box.state.parsedBody = body;
return box;
},
async (box) => {
const product = await database("product").where(
"id",
box.state.parsedBody.requestId
);
box.body = product;
body.statusCode = 200;
return box;
}
)();
The keys that will modify your response are the following
- body: will be the body that will be send by your HTTP response
- statusCode: will be the HTTP status code of your response
- cookies: will the the cookies attached to your response
- headers: will the the headers attached to your response
- multiValueHeaders: v1 support for multiValueHeaders
- isBase64Encoded: will indicate if your payload is Base64 encoded
Simple Response Helper
You can use a simpleResponse
function, only supply the HTTP response status code you like, the default is 200. The response body will stay status: "success"
exports.handler = lambdaFlow(simpleResponse())();
Will result in a 200 response like
{
status: "success"
}
Error Handling
How it Works
If you want to return an error to your user you need to attach it to the error key of the box. This will skip the execution of all other functions in your flow.
The error should be attach to the box and not throw, to control the flow in your application.
exports.handler = lambdaFlow(
(box) => {
const authorizationToken = box.event.headers.authorization;
if (!authorizationToken) {
box.error = {
exposed: true,
statusCode: 403,
message: "Not Authorized"
};
return box;
}
const body = JSON.parse(box.event.body);
box.state.parsedBody = body;
return box;
},
async (box) => {
const product = await database("product").where(
"id",
box.state.parsedBody.requestId
);
box.body = product;
return box;
}
)();
This will result in the following response with a HTTP status code of 403
{
"status": "error",
"message": "Not Authorized"
}
- The types of an error to attach to the error key of the box should looks like this:
- expose: a boolean property that indicate if you want to expose this error or not in the response
- statusCode: the error code, will be return as HTTP status code response
- error: the error itself, it's message property will be used in the response
type FlowError = {
expose: boolean;
statusCode: number;
message: string;
error?: Error;
};
(to help you with formatting errors see the error helpers section)
- If an error is not exposed it will return an HTTP status code of 500 with a "Internal Server Error" message like this
{
"status": "error",
"message": "Internal Server Error"
}
- If an unexpected error happens during your flow and you did not catch it will return the following response with a HTTP status code of 500
{
"status": "error",
"message": "Internal Server Error"
}
- It is recommended to use the error helper builder (next chapter) but there is basic compatibility with
http-errors
package (it will forward expose, statusCode and message), so you can do
box.error = new createHttpError.NotFound();
And it w ill return with an HTTP status code of 404
{
"status": "error",
"message": "Not Found"
}
Error Helpers
You can use little error helper to format the errors attached to the box.
- errorBuilder: the error builder will help you build the error to be returned to the user, it is a curried function so you can pass it's parameter one at the time.
- expose (default to false): a boolean property that indicate if you want to expose this error or not
- code (default to 500): the error code, will be return as HTTP status code response
- message or error (default to "Internal Server Error" message): the message you will send in the response or the error itself if it is an error it's message property will be used in the response
exemple 1:
box.error = errorBuilder()()()
Will return
{
exposed: false,
code: 500,
error: new Error()
}
Will result in this response with a HTTP status of 500
{
"status": "error",
"message": "Internal Server Error"
}
exemple 2:
box.error = errorBuilder(true)(422)(new Error("Could not process data"))
box.error = errorBuilder(true)(422)("Could not process data")
Will return
{
exposed: true,
code: 422,
error: new Error("Could not process data")
}
Will result in this response with a HTTP status of 422
{
"status": "error",
"message": "Could not process data"
}
Some predefined ones are derived from the builder but you can easaly create yours
- simpleError:
expose=false
and code=500
provided - exposedError:
expose=true
provided - nonExposedError:
expose=false
provided - notFoundError:
expose=true
and code=404
provided - notAuthorizedError:
expose=true
and code=403
provided - unprocessableError:
expose=true
and code=422
provided
Error builder in action
const notAuthorizedError = errorBuilder(true)(403);
exports.handler = lambdaFlow(
(box) => {
const authorizationToken = box.event.headers.authorization;
if (!authorizationToken) {
box.error = notAuthorizedError("You can't do that");
return box;
}
const body = JSON.parse(box.event.body);
box.state.parsedBody = body;
return box;
},
)();
Extra Error Handler
If you wish to have extra logic triggered when an error occurre (send log to remote place, call an other AWS service..) you can provide lambdaFlow
with an extra function.
exports.handler = lambdaFlow(
async (box) => {
try{
const product = await database("product").where(
"id",
box.state.parsedBody.requestId
);
box.body = product;
body.statusCode = 200;
} catch (error) {
box.error = notFoundError(new Error("Could not find this product"));
}
return box;
}
)((box) => {
sendLogs(box.error)
});
- In the error handler you will have access to the whole box that caused the error and the error itself
- The box in the error handler is a copy of the box that will be return, mutating it will not change the response
The Flow and it's Box Recap
A flow is similar to a pipe fonction in functional programming, you can combine your functions from left to right, and the box will flow thought them, you need to return the box at the end of your function so it can be passed on to the next function of the flow.
Those are the keys accessible inside the box
- event: is the event object generated by APIGateway
- context: is the context of the Lambda function executing
- callback: is a function that you can call in non-async lambda function handlers to send a response
- state: is a mutable key that you can use to pass data from one function to another
- error: you can attach an error to the error key, doing so will bypass other functions of the flow, only the error handler will be trigger, you can control wether you want to expose this error or not
Those are the keys of the box that you can change on the box to modify your response
- body: will the body that will be send by your HTTP response
- statusCode: will be the HTTP status code of your response
- cookies: will the the cookies attached to your response
- headers: will the the headers attached to your response
- multiValueHeaders: v1 support for multiValueHeaders
- isBase64Encoded: will indicate if your payload is Base64 encoded
If you want to pass data from one function to an other you can use the state key
The types looks like this:
type FlowBox = {
event: APIGatewayProxyEventV2;
context: Context;
callback: Callback<APIGatewayProxyResultV2>;
statusCode: number;
headers: {
[header: string]: boolean | number | string;
};
body: any;
multiValueHeaders: {
[header: string]: Array<boolean | number | string>;
};
isBase64Encoded: boolean;
cookies: string[];
state: any;
error: FlowError;
};
Typescript
By default the lambdaFlow
box
event will have the types for the version 2 of APIGateway proxy, if you want to use types for version 1 you can do the following.
lambdaFlow<APIGatewayProxyHandler>