On December 15, 2020, AWS announced the General Availability of AWS SDK for JavaScript Version 3 (here on referred to as V3). With exciting new features such as modular packages, a new middleware stack, and first-class TypeScript support, V3 is a welcomed update for JavaScript developers. However, for those who are accustomed to using the AWS SDK for JavaScript Version 2, V3 does introduce a new way of thinking about how we develop and write our code. In this article, we will try to gain a better understanding of V3 and how to use it. First, we will discuss the benefits of using V3. Secondly, we will get a better understanding of how to use V3. Third, we will look at an example of a function written using V3 and learn a few techniques to utilize when migrating from V2 to V3.
Benefits of V3
One of the greatest benefits that V3 brings to the table is the introduction of modular packaging. We can better understand the need for modular packaging by looking at how the SDK for Version 2 was used. Every AWS JavaScript project using the AWS SDK for JavaScript Version 2 included some variation of the code below, depending on the services required.
// AWS SDK for V2
const AWS = require("aws-sdk");
const dynamodb = new AWS.DynamoDB();
Since V2 of AWS SDK is published as a single package, the first import would import the entirety of the AWS SDK. Say, for example, you were writing a simple function to save a new user to DynamoDB. We will use this example throughout the rest of this article. The requirements for this function would be to have a DynamoDB client and the PutItem method. By importing V2 of the AWS SDK, you would not only receive your DynamoDB client and associated methods, but also code for services that were outside the scope of your application. Even the DynamoDB client above is much larger than what is needed for our simple function, which only requires the PutItem method. This issue reminds me of the famous quote about Object Oriented Programming from Joe Armstrong, creator of Erlang, who said "You wanted a banana but what you got was a gorilla holding the banana and the entire jungle." Although this quote is often taken out of context, as it is in this article, it does help convey the heaviness of the V2 SDK. This unnecessary import of the entire AWS SDK and DynamoDB client would have implications for the function's execution time and, for large scale production applications, the company's pocketbooks. Some services in V2 did have a form of modular packaging through the use of sub-modules, but this was not available for every service.
V3 solves this issue by restructuring the application to have a modular architecture, with a separate package for each service. The following code block is the V3 version of importing the DynamoDB service client.
Note: V3 is published on NPM under the scope '@aws-sdk'. All V3 imports will begin in this manner.
// AWS SDK for V3
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
Rather than importing the entire AWS SDK for every function, a developer can utilize V3's modular approach and only import the necessary services.
V3 takes this approach of modular packaging a step further. A developer can also import the specific service calls that are needed. This further reduces the bundle size of the final function. The following is an example of an import of the DynamoDB client Class and the PutItemCommand Class that would be needed for our earlier example. We will see these commands in action later in this article.
// AWS SDK for V3
const { DynamoDBClient, PutItemCommand } = require("@aws-sdk/client-dynamodb");
What are the performance benefits of the modular packaging approach of V3? In an AWS Developer blog post by Trivikram Kamat, Trivikram outlined the performance savings of V3. In a comparison of V2's monolithic import of the AWS SDK versus V3's modular approach for a JavaScript backend, the data showed about a 40% improvement in a function's execution time. On the frontend, the main chunk size of the AWS SDK in a production bundle went from approximately 395 KB and a 17 second load time for V2 to approximately 48 KB and a 3 second load time when using V3's modular packaging. The images below show the graphs of the data points mentioned above.
Comparison of lambda function execution times between V2 and V3 (in ms)
Load time of V2 SDK bundle (third file)
Load time of V3 SDK (third file)
Another major change of V3 is the first-class TypeScript support. Although users of V2 are able to utilize type definitions to write TypeScript code using the AWS SDK, the type definitions could go out of date. At the end of the day, the internal components of the V2 AWS SDK are written in vanilla JavaScript, not TypeScript. The AWS JavaScript SDK for Version 3 has been completely built using TypeScript. This change will enable developers to find type-related mistakes during development, resulting in fewer errors in production code.
Some other features of V3 include the use of async generation functions in paginators, the use of support for HTTP/2, and by default, the keeping of a Node.js HTTP connection alive without having to re-establish a TCP connection for future requests.
What do all of these changes mean for a developer and a company? After the initial shock of having to navigate and learn the new documentation, V3 will provide a developer with a better programming experience through the use of first-class TypeScript support, resulting in fewer bugs as a result of type errors.
Companies stand to gain much from the adoption of V3. As of December 1st, 2020, AWS Lambda reduced the billing granularity for Lambda function duration from 100ms to 1ms. This reduction in billing granularity, coupled with an, on average, 40% decrease in a function's execution time using V3, results in major cost savings on Lambda functions. For companies that utilize V3 on the frontend, the reduction in the bundle size will increase a website's performance, resulting in a better user experience and better SEO rankings.
Features of the AWS SDK for Version 3
Let's take a closer look at some of the features of V3 and examine some code.
All requests made through the SDK are asynchronous. It is important to ensure that your code is written in a way to manage these asynchronous calls. Those familiar with V2 recall the complexity of handling these async calls using callbacks and the .promise method. V3 attempts to simplify this process by recommending that all async functions use the async/await pattern, although promises and callbacks can be used if preferred. To utilize the async/await pattern, our service calls must be wrapped in an async function. To facilitate this, the documentation often shows service calls wrapped by an async arrow function that is immediately invoked, or as an immediately invoked function expression (IIFE). When using V3 with lambda functions, you can make your handler function an async/await function. We will be using the async/await pattern in our example later in the article.
V3 provides access to AWS services through a collection of client classes. From these client classes, you can create service interface objects, commonly called service objects. All client classes imports begin with the same prefix '@aws-sdk/client-'. Each service provides a client class with a send method that you use to invoke every API the service supports.
To better help understand the meaning of these terms, let's look at a sample workflow and code that corresponds to these terms.
The basic workflow of using V3 is as follows:
- Import your service client and any needed client methods
- Initialize your service object using the service client with any desired configuration
- Write your service call by calling the client method using the new keyword (Optional) Instead of writing your service call by calling the client method, you could initialize a service command with your parameters as input
- Send your request using the send method on your service object
// 1. Import our service client class (DynamoDBClient) and our client method (PutItemCommand)
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
// 2. Create your service object (ddbClient) using the service client class
// (DynamoDBClient)and desired configuration (region: 'us-east-2')
const ddbClient = new DynamoDBClient({ region: 'us-east-2' });
// 3 and 4. Write your service call by calling the client method using the new keyword
// (new PutItemCommand(params)). Send your service call using the send method on
// your service object (ddbClient)
// (Optional) Initialize a service command with your parameters as input
// Service call using client method and new keyword
const data = await ddbClient.send( new PutItemCommand(params) );
// Service call after initializing a service command with parameters as input
const putCommand = new PutItemCommand({params});
const data = await ddbClient.send(putCommand);
One helpful feature of V3 is that operations can be performed in V3 using either V2 or V3 commands. For those who prefer to use a non-modular interface, an approach with more of a V2 feel, you can import the client with only the service name and call the operation name directly from the client. Notice the example below.
const { DynamoDB } = require("@aws-sdk/client-dynamodb");
const ddbClient = new DynamoDB();
...
const data = await ddbClient.putItem(params);
V2 documentation can be used to see the methods available to use with the above approach. While using the above approach will result in an increase of your function's performance, you will not see the full benefits as you would from utilizing the full modular packaging of V3.
An Example
Let's see this process in whole by using an example. Let's write the code for our original example, a simplified function that takes some user data and saves that data to a DynamoDB table. For the sake of illustration, let's imagine it's Michael Scott's first day at Dunder Mifflin and his information is being added to the company database.
In the function below, we first import our DynamoDBClient and our PutItemCommand from the V3 NPM package. Next, we initialize our ddbClient, passing in our region as a parameter. We then define our parameters for our PutItemCommand call, where we have defined our table name and the attributes we want for our new user. Finally, we define our function, hireMichael. In our function, we use a try/catch block to save our new user to the DunderMifflinEmployees table in DynamoDB. At the end, we invoke our hireMichael function to run the command.
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
const ddbClient = new DynamoDBClient({ region: 'us-east-2' });
const params = {
TableName: 'DunderMifflinEmployees',
Item: {
user_id: { N: '1' },
name: { S: 'Michael Gary Scott' }
},
};
const hireMichael = async () => {
try {
const data = await ddbClient.send(new PutItemCommand(params) );
console.log({ data });
return data;
} catch (err) {
console.error(err);
}
};
hireMichael();
For more examples of how to use the V3 SDK with other AWS services, I recommend looking at the reference entitled 'SDK for JavaScript code examples' below.
Migrating from V2 to V3
To take advantage of the benefits of V3, it doesn't necessarily require a complete rewrite of an application written in V2. AWS recommends three paths to help developers migrate to V3. Path 1 is the easiest to implement, while path 3 would require more code rewrites.
Path 1: Performs minimal changes. Replace your V2 SDK imports with the specific AWS Service packages you need. Create and use V3 service clients, replacing global values, such as region, with configuration values passed in as arguments to the client. Continue to use the callback and promise pattern used in V2.
Path 2: Follow path 1 and remove .promise from any function calls.
Path 3: Follow path 1 and rewrite functions to use the async/await programming model.
Since V2 commands are still valid in V3, it is possible to slowly migrate applications over to V3 as needed following any of the paths above.
Conclusion
The AWS SDK for JavaScript Version 3 is a welcomed update to the world of AWS. As companies and developers continue to incorporate V3 into their applications, they will surely see benefits both in the developer experience and cost savings.
Thank you for taking the time to read this article. Your feedback is appreciated! If there is something I got wrong, something that isn't clear or you just want to reach out and connect, feel free to leave a comment below or on Twitter.