Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Hosted-services / AWS

Simplify and Secure Your Node.js AWS Lambda Code - Part 2

5.00/5 (1 vote)
19 Apr 2016CPOL4 min read 12.9K  
Part 2 of our series on simplifying and securing AWS Lambda code using Node.js

Second Part In A Three Part Series

Consider checking out part 1 if you haven’t already.

In part 1 of this series, we learned how to use vandium, the open source npm module, to quickly add validation and injection attack protection to Node.js Amazon Web Services (AWS) Lambda.

Our Vandium Wrapped Lambda Handler

The following code is the final code from part 1 of this series. Part 1 ended with wrapping an AWS Lambda handler with vandium. The handler itself consisted of a series of nested asynchronous callback functions used to send a message from one user to another.

JavaScript
'use strict';

const vandium = require( 'vandium' );

// our token validation service
const tokenValidator = require( './lib/token-validator' );

// our nosql datastore
const db = require( './lib/db' );

// message service
const msgService = require( './lib/messages' );

vandium.validation( {
    
        userId: vandium.types.uuid().required(),
        
        message: vandium.types.string().min( 1 ).max( 255 ).required()
    });

exports.handler = vandium( function( event, context, callback ) {
    
    tokenValidator.validateToken( event.token, function( err, result ) {
    
        if( err ) {
      
            return callback( err );
        }
    
        // sender id
        let senderId = result.userId;

        db.userExists( event.userId, function( err, result ) {
      
            if( err ) {
        
                return callback( err );
            }
      
            msgService.send( senderId, event.userId, event.message, function( err, result ) {
        
                if( err ) {
          
                    return callback( err );
                }
        
                callback( null, 'sent' );
            });
        });
    });
});

Although this code is functional, it might not be that easy to maintain if we added new functionality in the future. The reason why potential issues may occur is that the structure of the code is beginning to resemble what’s known in the Node.js world as “callback hell”. To avoid this pattern, the JavaScript language introduced Promises.

Promises

The use of Promises eliminates our callback hell while improving the maintainability and testability of our code.

Let’s look at the traditional Node.js callback pattern:

JavaScript
doThisFirst( 'start', function( err, result ) {
    
    if( err ) {
        
        console.log( err );
        return;
    }
    
    doThisSecond( result.two, function( err, result ) {
       
        if( err ) {
            
            console.log( err );
            return;
        } 
        
        doThisThird( result.three, function( err, result ) {
            
            if( err ) {
                
                console.log( err );
                return;
            }
            
            console.log( 'result:', result );
        });
    });
});

The callback pattern in the above example will now be replaced with Promises to simultaneously reduce the amount of code while increasing the code’s readability. Since we’re modifying existing code and we don’t want to change our library code, we can use the excellent bluebird library to “Promisify” our code.

Our “promisified” code from the previous example will now look like:

JavaScript
const Promise = require( 'bluebird' );

// need to "promisify" our callbacks
const doThisFirstAsync = Promise.promisify( doThisFirst );
const doThisSecondAsync = Promise.promisify( doThisSecond );
const doThisThridAsync = Promise.promisify( doThisThird );

doThisFirstAsync( 'start' )
    .then( function( result ) {
        
        return doThisSecondAsync( result.two );
    })
    .then( function( result ) {
        
        return doThisThirdAsync( result.three );
    })
    .then( function( result ) {
        
        console.log( 'result:', result );
    })
    .catch( function( err ) {
        
        console.log( err );
    });

Now, not only is the code smaller, it is also easier to follow. The one thing that you might be noticing are the “Async” suffixes on the functions. These are Promise-friendly versions of our existing code that bluebird created for us.

Lambda and Promises

Now that we know what Promises are, let’s modify our Lambda handler.

JavaScript
'use strict';

const vandium = require( 'vandium' );

const bluebird = require( 'bluebird' );

// our token validation service - promisified
const tokenValidator = bluebird.promisifyAll( require( './lib/token-validator' ) );

// our nosql datastore - promisified
const db = bluebird.promisifyAll( require( './lib/db' ) );

// message service - promisified
const msgService = bluebird.promisifyAll( require( './lib/messages' ) );

vandium.validation( {
    
        userId: vandium.types.uuid().required(),
        
        message: vandium.types.string().min( 1 ).max( 255 ).required()
    });

exports.handler = vandium( function( event, context, callback ) {
    
    tokenValidator.validateTokenAsync( event.token )
        .then( function( result ) {
            
            let senderId = result.userId;
            
            return db.userExistsAsync( event.userId );
                .then( function() {
                    
                    return msgService.sendAsync( senderId, event.userId, event.message );
                });
        })
        .then( function( result ) {
            
           callback( null, 'ok' ); 
        })
        .catch( function( err ) {
            
           callback( err ); 
        });
});

As with the previous Promise example, our code is a lot flatter, easier to follow and there’s less code. Notice that we have a nested Promise after the initial “tokenValidator” Promise. We do this because we need to access to the “senderId” after we determine if the user exists. When nesting promises, the return value of the inner promise instance will be routed to the next “then” in the outer instance.

But there’s one thing that just doesn’t feel right?—?usage of the callback function at the end of the Lambda handler to indicate successful or failed execution. Ideally, Lambda handlers would support the Promise pattern and would allow us to simply return a value at the end of the handler, instead of having to use the cumbersome callback function. Unfortunately Lambda handlers do not support Promises, but vandium does.

Promises as First Class Citizens

Vandium’s focus is to reduce the amount of code that one needs to maintain, while providing robustness, security and functionality. Promises are treated as first class citizens when you use vandium. To use this functionality, simply return the promise and vandium takes care of the rest.

When vandium wraps the handler, it automatically routes successful and failed promises to the callback handler for you. This is important because it removes the need for the developer to manually route values to the Lambda callback function for every single success or failure case. Naturally, this allows the developer to focus more on writing the actual logic that needs to be performed in the Lambda handler.

The following is our “Promisified” Lambda handler, wrapped with vandium, the return value (or error) is automatically routed to the Lambda callback.

JavaScript
'use strict';

const vandium = require( 'vandium' );

const bluebird = require( 'bluebird' );

// our token validation service - promisified
const tokenValidator = bluebird.promisifyAll( require( './lib/token-validator' ) );

// our nosql datastore - promisified
const db = bluebird.promisifyAll( require( './lib/db' ) );

// message service - promisified
const msgService = bluebird.promisifyAll( require( './lib/messages' ) );

vandium.validation( {
    
        userId: vandium.types.uuid().required(),
        
        message: vandium.types.string().min( 1 ).max( 255 ).required()
    });

exports.handler = vandium( function( event ) {
    
    return tokenValidator.validateTokenAsync( event.token )
        .then( function( result ) {
            
            let senderId = result.userId;
            
            return db.userExistsAsync( event.userId );
                .then( function() {
                    
                    return msgService.sendAsync( senderId, event.userId, event.message );
                });
        })
        .then( function() {
            
            return 'ok';
        });
});

Note that we now return the Promise itself back to the vandium wrapper. Vandium will take care of determining the result of the Promise. In the case of a successful completion, or in the case of an error, vandium will automatically route the resulting value of either outcome back to the Lambda handler’s callback function. Also, our code is further simplified by removing the context and callback parameters since they are no longer needed.

We started off with 51 lines of nested callback code and are now left with less than 40 lines of code that have a significantly simplified flow. Having fewer lines of code and having a simpler branch structure results in easier maintenance and testing.

Wrapping Up Part 2

Using Promises in our Lambda handlers reduces the amount of complexity and the amount of code that we need to maintain and test. Although AWS Lambda’s Node.js implementation is great, its lack of support for easy use of Promises causes writing Lambda handlers to be unnecessarily difficult and also causes the actual code for the handlers to be inelegant. Vandium treats Promises as first class citizens and makes it easier than ever to use them with Lambda handlers.

To be continued in Part three

For more information about vandium, visit the project site.

Special thanks to @mikey_s_e for editing this article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)