1
votes

I am struggling to understand a basic aspect of Lambda implementation.

Problem: how to use a lambda both inside and outside of an API context?

I have a lambda (nodejs) with an API gateway in front of it:

 MyFunction:
    Type: AWS::Serverless::Function
    Properties:
        CodeUri: functions/myfunction/
        Handler: app.lambdaHandler
        Runtime: nodejs14.x
        Timeout: 4
        Policies:
            - DynamoDBCrudPolicy:
                  TableName: MyTable
        Events:
            ApiEvent:
                Type: Api
                Properties:
                    Path: /myfunc
                    Method: any

The handler is used to read (GET) or write (POST) into a a DynamoDB table and returns accordingly. If no method is passed it assumes GET.

exports.lambdaHandler = async (event) => {

    const method = event.httpMethod ? event.httpMethod.toUpperCase() : "GET";
    
    try {
        switch (method) {
            case "GET":
                // assuming an API context event.queryStringParameters might have request params. If not the parameters will be on the event
                const params = event.queryStringParameters ? event.queryStringParameters : event;
                // read from dynamo db table then return an API response. what if outside an API context?
                return {
                    statusCode: 200,
                    body: JSON.stringify({ message: "..." })
                };
            case "POST":
                // similarly the body will be defined in an API context
                const body = typeof event.body === "string" ? JSON.parse(event.body) : event.body;
                // write to dynamo db table
                return {
                    statusCode: 200,
                    body: JSON.stringify({ message: "..." })
                };
            default:
                return {
                    statusCode: 400,
                    body: JSON.stringify({ error: "method not supported" })
                };
        }
    } catch (error) {
        // this should throw an Error outside an API context
        return {
            statusCode: 400,
            body: JSON.stringify({ error: `${typeof error === "string" ? error : JSON.stringify(error)}` })
        };
    }
}

Is there an easy way to refactor this code to support both scenarios? For example a step function could call this lambda as well. I know I can have the step function invoking an API but I think this is overkill as step functions support invoking lambdas directly.

I see 2 ways I can go about this:

  1. The lambda has to be aware of whether it is being invoked within an API context or not. It needs to check if there's a http method, queryStringParameters and build its input from these. Then it needs to return a response accordingly as well. A stringified JSON with statusCode or something else, including throwing an Error if outside an API call.

  2. The lambda assumes it is being called from an API. Then the step function needs to format the input to simulate the API call. The problem is that the response will be a string which makes it difficult to process inside a step function. For example assigning it to a ResultPath or trying to decide if there was an error or not inside a choice.

Additionally I could have the step function calling an API directly or the last resort would be to have 2 separate lambdas where the API lambda calls the other one but this will incur additional costs.

Thoughts? Thanks.

1
Why must the same Lambda be called for both scenarios? :) - Chris Williams
:-) because of all stereotypical aspects of a developer (me): laziness, over engineering, simplification at all costs... On a serious note, it feels weird to create separate lambdas for this as the core logic is the same, it's only the input and output that need to be manipulated depending on the caller. - Rsrvrdog
Perhaps creating a shared dependency library might be better? With the Lambda then just calling the library to pass in inputs vs having the Lambda itself contain logic that would need to be reused across multiple inputs. - Chris Williams
Yes @ChrisWilliams that's something that can be done but I'll also need to have a library to generate an appropriate response. I truly thought this would be a very standard use case and AWS would have some way of abstracting the input/output from the lambda function but I can't find anything about that. - Rsrvrdog

1 Answers

0
votes

This is where middlewares like MIDDY come into picture.

All the logic to determine event type, event source and parsing will be abstracted out and actual business logic always coded to use standard input. we can add as many layers as we need and send standard schema as lambda input. Typically, events may come from Api Gateway, Step function, Kinesis, SQS, etc and same lambda works for any event source.

export const handler = middy((event, context, callback) => {
    const mainProcess = async () => {
        const response = {}
        // Busines Logic using event
        return response;
    };

    mainProcess()
        .then((result) => {
            callback(null, result);
        })
        .catch((error) => {
            callback(error);
        });
})
    .use({
        before: (hndlr, next) => {
            const parsedEvent = parseApiGatewayRequest(hndlr.event);
            if (parsedEvent) {
                hndlr.event = parsedEvent;
            }
            next();
        },
    })
    .use({
        before: (hndlr, next) => {
            const parsedEvent = parseStepFuncRequest(hndlr.event);
            if (parsedEvent) {
                hndlr.event = parsedEvent;
            }
            next();
        },
    });