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.
'use strict';
const vandium = require( 'vandium' );
const tokenValidator = require( './lib/token-validator' );
const db = require( './lib/db' );
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 );
}
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:
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:
const Promise = require( 'bluebird' );
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.
'use strict';
const vandium = require( 'vandium' );
const bluebird = require( 'bluebird' );
const tokenValidator = bluebird.promisifyAll( require( './lib/token-validator' ) );
const db = bluebird.promisifyAll( require( './lib/db' ) );
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.
'use strict';
const vandium = require( 'vandium' );
const bluebird = require( 'bluebird' );
const tokenValidator = bluebird.promisifyAll( require( './lib/token-validator' ) );
const db = bluebird.promisifyAll( require( './lib/db' ) );
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.