<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[LHidalgo.dev]]></title><description><![CDATA[💻 Full Stack Software Engineer and ☁ Serverless Developer, focused on building efficient and cost-effective applications using cloud-based technologies]]></description><link>https://lhidalgo.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 18 Apr 2026 01:49:05 GMT</lastBuildDate><atom:link href="https://lhidalgo.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[[Video] DynamoDB for Relational People - AWS Community Day Turkiye 2025]]></title><description><![CDATA[https://www.youtube.com/watch?v=H873D6t0D9k
 
Video Description
Having worked closely during the last years implementing and fixing DynamoDB implementations, I've been able to see some recurring errors and misunderstandings, and the root cause in mos...]]></description><link>https://lhidalgo.dev/video-dynamodb-for-relational-people-aws-community-day-turkiye-2025</link><guid isPermaLink="true">https://lhidalgo.dev/video-dynamodb-for-relational-people-aws-community-day-turkiye-2025</guid><category><![CDATA[DynamoDB]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[SQL]]></category><category><![CDATA[NoSQL]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Fri, 18 Apr 2025 22:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759391824928/f05a85d0-5353-4abb-bef6-3ef66e2f094f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=H873D6t0D9k">https://www.youtube.com/watch?v=H873D6t0D9k</a></div>
<p> </p>
<h2 id="heading-video-description">Video Description</h2>
<p>Having worked closely during the last years implementing and fixing DynamoDB implementations, I've been able to see some recurring errors and misunderstandings, and the root cause in most scenarios was trying to use it as you would with an SQL database. The goal of the task is to highlight the differences between SQL and NoSQL databases and showcase the most common errors with easy ways to fix them.</p>
<p>Organized by Cloud Turkiye<br />X <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbk1ONUVPbk5GNVl4b2k4aXJiektCMXFxcFR3d3xBQ3Jtc0tsYW5lTnYwc2JHZnNDekpmUDlGbkhGYlhvandVaDhIYjdjMDkwNEhYX3BfN0RmZVJqNEhMRnc2VHd3YWlaeXpidGhtWDdjSjRYTWpLcFpYUzZSRmFXTXh4ZTNkVHNBeFZRb3dzMXYyWGhkeThidHBSSQ&amp;q=https%3A%2F%2Fx.com%2FCloudTurkiye&amp;v=H873D6t0D9k">https://x.com/CloudTurkiye</a><br />Kommunity <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqa19tQlo1cnJpaVNiYk1sUXR2a2ZvamxYOU5ad3xBQ3Jtc0tudDUxVnkxUUNyTTRsdHRTZWF6LXZXa2pIVmd4WFRkbnhJNU5nSWtyV0JLNF85WW1sMVlWbkxnSHNiY3dwdU5XemRPZGh3b2xCaGhtdGc1aGdPUXA2TUo4cV9PUDB4TUthVnRUS0FlQ3d5bjdfOHdqcw&amp;q=https%3A%2F%2Fkommunity.com%2Fcloud-turkiye&amp;v=H873D6t0D9k">https://kommunity.com/cloud-turkiye</a></p>
]]></content:encoded></item><item><title><![CDATA[How to Create a URL Shortener with Custom Analytics using AWS Services]]></title><description><![CDATA[TL;DR; This article covers how to implement a basic URL Shortener API with Custom Metrics. The sample project implements a Serverless Solution, deployed with the help of AWS CDK that relies on API GateWay, DynamoDB, Lambdas and Custom Cloudwatch metr...]]></description><link>https://lhidalgo.dev/url-shortener-custom-metrics</link><guid isPermaLink="true">https://lhidalgo.dev/url-shortener-custom-metrics</guid><category><![CDATA[CDK]]></category><category><![CDATA[DynamoDB]]></category><category><![CDATA[serverless]]></category><category><![CDATA[API Gateway]]></category><category><![CDATA[Lambda function]]></category><category><![CDATA[analytics]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Sun, 19 Jan 2025 23:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738497288511/84d9cec5-210c-4690-a216-9f135a3d4c6a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR;</strong> This article covers how to implement a basic URL Shortener API with Custom Metrics. The <a target="_blank" href="https://github.com/Lorenzohidalgo/url-shortener-custom-metrics">sample project</a> implements a Serverless Solution, deployed with the help of AWS CDK that relies on API GateWay, DynamoDB, Lambdas and Custom Cloudwatch metrics.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>Have you ever wondered how URL shorteners work under the hood? Or, even better, how to implement a similar solution that allows you to create custom analytics based on user behavior and types?</p>
<p>In this article, we’ll discuss what is needed to implement such features and provide a step-by-step guide (with a <a target="_blank" href="https://github.com/Lorenzohidalgo/url-shortener-custom-metrics">complete sample repository</a>) for deploying them on your account.</p>
<h1 id="heading-building-the-api">Building the API</h1>
<h2 id="heading-overview">Overview</h2>
<p>The API will have two different endpoints:</p>
<ul>
<li><p>A private endpoint to allow for redirect configurations to be created</p>
</li>
<li><p>A public endpoint to redirect users to the desired URL</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738489767372/696c8fd6-07ff-4cee-a083-b5d10d8249d6.png" alt="Diagram illustrating a URL redirect architecture using AWS services. Route 53 directs to API Gateway, which handles requests with two Lambda functions: CreateRedirectURL Lambda and RedirectURL Lambda. Data is logged to CloudWatch, and URL data is stored in a Redirects Table. An API key is used for authentication." class="image--center mx-auto" /></p>
<p>As one can see in the provided infrastructure Diagram, the resources required to build the API are very straightforward.</p>
<p>To build the API we took advantage of serverless AWS services, such as API Gateway, Lambdas, and DynamoDB.</p>
<p>The Infrastructure was designed as a proof-of-concept, but one could further improve it by adding IAM authentication, CloudFront, or WAF to further protect the API infrastructure.</p>
<h2 id="heading-custom-domain-with-route53">Custom Domain with Route53</h2>
<p>A custom Domain and an already configured Hosted Zone are prerequisites to deploying the provided CDK project, but one could easily remove that configuration and test the infrastructure with the default API Gateway domain.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738489824235/9c4b295e-4d2b-4d01-a982-308dd11e8736.png" alt="Diagram showing a connection between Route 53 and an API Gateway. The text &quot;Hosted Zone, Domain Certificate, A Record&quot; is in between." class="image--center mx-auto" /></p>
<p>As part of the project, we use Route53 to configure a custom domain for our API Gateway, since using the default domain wouldn’t act as a “URL Shortener” for most scenarios.</p>
<p>The CDK project expects the Hosted Domain to be already configured, but it will automatically generate the Domain Certificate and A Record for the new API endpoints to allow for HTTPS traffic.</p>
<h2 id="heading-redirects-dynamodb-table">Redirects DynamoDB Table</h2>
<p>A Single DynamoDB Table will be used to store the required redirect configurations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738490169242/b9c59342-cc02-44cf-bd17-af5885e542a6.png" alt="Interface displaying database model details for &quot;ShortenedUrls.&quot; Includes sections for primary key attributes with &quot;urlId&quot; as a string partition key and other attributes like &quot;ttl,&quot; &quot;originalURL,&quot; and &quot;ttlInSeconds&quot; with their types and sample data formats." class="image--center mx-auto" /></p>
<p>The Data model used by the sample project is straightforward, storing only a few attributes:</p>
<ul>
<li><p><code>urlId</code> - Used as PrimaryKey and also as part of the shortened URL. The redirect URLs are generated as <code>https://link.&lt;your_url&gt;/&lt;urlId&gt;</code></p>
</li>
<li><p><code>ttl</code> - Timestamp used to identify when the redirect record should be removed from the DynamoDB table</p>
</li>
<li><p><code>originalURL</code> - The actual URL to which incoming requests should be redirected to</p>
</li>
<li><p><code>ttlInSeconds</code> - Number of seconds for which the record was created for</p>
</li>
</ul>
<p>The DynamoDB table is also configured to use the pay-per-request pricing model, time to live enabled on the TTL attribute, and to be deleted once the stack is removed.</p>
<h2 id="heading-create-url-endpoint">Create URL endpoint</h2>
<p>The Create URL endpoint will be the one responsible for creating and saving redirect configurations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738488877677/3648b9bd-f820-4bbb-bb96-6d0671c65d8e.png" alt="Flowchart showing user interaction with an API Gateway, protected by an API key. The user sends a PUT request, which goes through the gateway to a Lambda function named &quot;CreateRedirect URL Lambda,&quot; which then updates a Redirects Table." class="image--center mx-auto" /></p>
<p>The configuration for this endpoint will be very straightforward, it will be triggered for any POST request received on the configured domain that provides a given <code>urlId</code> - such as <code>https://link.&lt;your_url&gt;/&lt;urlId&gt;</code>.</p>
<p>This endpoint will be private, requiring an API Key to be called, and a <a target="_blank" href="https://github.com/Lorenzohidalgo/url-shortener-custom-metrics/blob/main/lib/url-shortener-custom-metrics-stack.js#L125">JSON schema</a> was configured to allow API Gateway to <a target="_blank" href="https://github.com/Lorenzohidalgo/url-shortener-custom-metrics/blob/main/lib/url-shortener-custom-metrics-stack.js#L155">validate any incoming request body</a>.</p>
<p>Having API Gateway validating incoming requests allows for faster response times (in case of errors) and avoids invocations of the Lambda function for invalid requests.</p>
<p>The CreateRedirectURL Lambda in the sample project implements a basic approach, where it will:</p>
<ol>
<li><p>Ensure the received input is correct</p>
</li>
<li><p>Build and send a conditional <code>PUT</code> request to DynamoDB, a condition expression will be added to avoid overwriting existing records with the same primary key</p>
</li>
<li><p>Based on the response:</p>
<ol>
<li><p>DynamoDB operation succeeded —&gt; Build the success response and return the newly created redirect URL</p>
</li>
<li><p>Operation failed —&gt; Map the error and build the error response</p>
</li>
</ol>
</li>
</ol>
<p>As future improvements, one could implement more validations, link the created records to a given user / API Key, or even add some custom metrics to, for example, record how many redirects are created for given domains.</p>
<h2 id="heading-redirection-endpoint">Redirection endpoint</h2>
<p>The redirect URL endpoint is public and is the one responsible for creating the metrics and redirecting the user to the appropriate URLs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738488896464/e832e4a1-9203-4cd5-8bb9-fc8f55c1c0e6.png" alt="Architecture diagram illustrating a user sending a GET request to an API Gateway public endpoint, which connects to a RedirectURL Lambda function. The function interacts with a Redirects Table to retrieve data and sends metrics to CloudWatch." class="image--center mx-auto" /></p>
<p>This endpoint is configured to be triggered for every GET request received on the configured domain that provides a given <code>urlId</code> - such as <code>https://link.&lt;your_domain&gt;/&lt;urlId&gt;</code>.</p>
<p>The responsibilities of this Lambda are:</p>
<ol>
<li><p>Validating incoming requests to ensure a valid path parameter has been provided.</p>
</li>
<li><p>Retrieve the redirect configuration from DynamoDB based on the provided <code>urlId</code></p>
</li>
<li><p>Inspect the request headers and DynamoDB response to generate and store the custom metrics on CloudWatch</p>
</li>
<li><p>Based on the DynamoDB response</p>
<ol>
<li><p>If a redirect record was found —&gt; return the success response with redirect configuration</p>
</li>
<li><p>no record found or DynamoDB request failed —&gt; Map and return the appropriate error response</p>
</li>
</ol>
</li>
</ol>
<p>The “magic” of this Lambda is how the success response is built, which allows browsers to redirect users to the provided URL. For Example:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"statusCode"</span>: <span class="hljs-number">302</span>,
    <span class="hljs-attr">"headers"</span>: {
        <span class="hljs-attr">"Location"</span>: <span class="hljs-string">"https://lhidalgo.dev/url-shortener-custom-metrics"</span>
    }
}
</code></pre>
<p>The redirection works thanks to the returned status code, as returning <code>3XX</code> lets the browser know that the requested URL has been redirected. For our scenarios, the relevant <code>3XX</code> status codes are:</p>
<ul>
<li><p><code>301</code> —&gt; Tells the browser that the URL has been moved <strong><em>permanently</em></strong>, the browser might use cached responses on consequent requests.</p>
</li>
<li><p><code>302</code> —&gt; Resource has been moved <strong><em>temporarily</em></strong>, the browser will always call the original URL in consequent requests.</p>
</li>
</ul>
<p>Since we want to track how many times a given URL is requested and create metrics based on the request information, the sample project will use the <code>302</code> status code. For other scenarios, where metrics are not implemented, or only unique user requests are relevant, using the <code>301</code> status code would be recommended to reduce the amount of received requests.</p>
<h1 id="heading-custom-metrics">Custom Metrics</h1>
<h2 id="heading-overview-1">Overview</h2>
<p>AWS CloudWatch will by default record some basic usage and performance metrics of deployed services, such as Lambda Functions, where metrics such as Invocation Count, Duration, and Error vs. Success Rate are automatically collected.</p>
<p>A part of the default metrics, CloudWatch also allows you to create custom metrics to track specific aspects of your application.</p>
<p>When working with custom metrics, one should be aware of the format in which they are stored, a quick overview of the most relevant aspects would be:</p>
<ul>
<li><p>Namespace —&gt; could be seen as categories where multiple metrics can be created, especially useful for categorizing related metrics or differentiating them by service or feature.</p>
</li>
<li><p>Metric Name —&gt; names to identify specific metrics, for example <code>RedirectRequest</code></p>
</li>
<li><p>Unit &amp; Value —&gt; attributes to inform Cloudwatch what and in what format is being recorded and the actual value for the same</p>
</li>
<li><p>Dimensions —&gt; additional information, provided as Key-Value pairs, that provides additional data to the given metric.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Developers should be very mindful when implementing custom metrics, as the <a target="_self" href="https://aws.amazon.com/cloudwatch/pricing/">cost can escalate quickly</a>. When using Dimensions, the <strong><em>metric count is the equivalent of every unique combination of Metric names and Dimensions</em></strong>.</div>
</div>

<h2 id="heading-implementing-custom-metrics">Implementing Custom Metrics</h2>
<p>There are multiple ways to send custom metrics to CloudWatch, a quick overview of the different options would be:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><strong>Benefits</strong></td><td><strong>Drawbacks</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Log Filter Subscription Metric</strong></td><td>Can be enabled, disabled, and changed without modifying the code. Completely async, doesn’t add any additional execution time to the requests.</td><td>Depends on CloudWatch Logs being enabled, can be difficult to configure without structured logs.</td></tr>
<tr>
<td><strong>Lambda Powertools Metrics package</strong> (<a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html">CloudWatch Embedded metric format</a>)</td><td>Package maintained by AWS. High customization potential. No async-calls are required.</td><td>Depends on CloudWatch Logs being enabled, can be difficult to configure without structured logs. Require source code changes in order to add them.</td></tr>
<tr>
<td><strong>AWS-SDK CloudWatch PutMetricCommand</strong></td><td>Most customizable approach. CloudWatch API requests can send multiple metrics at once. CloudWatch logs can be disabled.</td><td>Require source code changes in order to add them. Require an async API request in order to emit metrics, adding some latency to the execution time.</td></tr>
</tbody>
</table>
</div><p>The provided sample project implements custom CloudWatch metrics using the <code>aws-sdk</code> v3 to keep the sample as <em>vanilla</em> as possible, the <a target="_blank" href="https://docs.powertools.aws.dev/lambda/typescript/latest/core/metrics/">Lambda PowerTools metrics package</a> is recommended for any scenario where response times are critical, as no additional latency is introduced.</p>
<p>As part of our example, we create a single metric called <code>RedirectRequest</code> which includes the following dimensions:</p>
<ul>
<li><p><code>shortName</code> —&gt; <code>urlId</code> / <code>pathparameter</code> of the redirect URL</p>
</li>
<li><p><code>platform</code> —&gt; device platform/OS, fetched from the request headers</p>
</li>
<li><p><code>deviceLanguage</code> —&gt; device language, fetched from the request headers</p>
</li>
<li><p><code>browser</code> —&gt; browser used, fetched from the request headers</p>
</li>
<li><p><code>domain</code> —&gt; the domain of the redirection URL, f.e.: <code>lhidalgo.dev</code></p>
</li>
<li><p><code>redirectURL</code> —&gt; the full URL where the user will be redirected to, f.e.: <code>https://lhidalgo.dev/url-shortener-custom-metrics</code></p>
</li>
<li><p><code>success</code> —&gt; flag to identify if the user was successfully redirected or not</p>
</li>
</ul>
<p>Having these dimensions will allow us to implement different analyses, f.e.: language distribution of users accessing <code>lhidalgo.dev</code>.</p>
<h2 id="heading-consuming-custom-metrics">Consuming Custom Metrics</h2>
<p>CloudWatch metrics can be exported and consumed from third-party dashboards and analytics platforms, but the easiest approach is to use the AWS Cloudwatch Console, using the console one can explore all the available metrics and even create a custom dashboard to ease the recurrent access to given metrics.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738496454742/14d9914b-4aff-4d10-bb33-6272b3931398.png" alt="A screenshot of a metric query interface showing redirect data. It lists redirects to lhidalgo.dev, serverlessguru.com, and Google, displaying minimum, maximum, sum, and average values. Options to add queries, dynamic labels, and math functions are visible, with additional settings for statistics, period, and timezone." class="image--center mx-auto" /></p>
<p>As one can see in the above screenshot, the CloudWatch console also allows us to implement <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html">Metric Math expressions</a> to aggregate the metric data to our desired format.</p>
<p>Taking advantage of this one can see how, thanks to the dimensions we provided to the metrics, we can now create a table view to analyze the redirect count to the different domains.</p>
<h1 id="heading-deploying-the-provided-repository">Deploying the provided repository</h1>
<p>If you want to deploy the stack that was created as part of this article, feel free to fork the <a target="_blank" href="https://github.com/Lorenzohidalgo/url-shortener-custom-metrics">public repository</a> and follow the next steps.</p>
<h2 id="heading-requirements">Requirements</h2>
<p>To be able to deploy and use this application without requiring any changes you'll need:</p>
<ul>
<li><p>Node JS</p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/hello_world.html#hello_world_bootstrap">CDK &amp; Bootstrapped AWS Account</a></p>
</li>
<li><p>A Custom Domain and a configured Route 53 Hosted Zone</p>
</li>
</ul>
<h2 id="heading-deployment-steps">Deployment steps</h2>
<ol>
<li><p>Fork &amp; Clone the repository</p>
</li>
<li><p>Install the project dependencies <code>npm i</code></p>
</li>
<li><p>Set the required environment variable <code>DOMAIN_NAME</code> to your custom domain. Examples:</p>
<ul>
<li><p>Windows/CMD - <code>set DOMAIN_NAME=</code><a target="_blank" href="http://domain.com"><code>domain.com</code></a></p>
</li>
<li><p>Bash - <code>export DOMAIN_NAME=</code><a target="_blank" href="http://domain.com"><code>domain.com</code></a></p>
</li>
</ul>
</li>
<li><p>Synthesize the Cloudformation template to ensure the configuration is correct <code>npx cdk synth</code></p>
<ul>
<li>Some errors might be thrown if the Route53 Hosted Zone doesn't exist or your current AWS role lacks the proper access to it</li>
</ul>
</li>
<li><p>Deploy the application <code>npx cdk deploy</code></p>
</li>
<li><p>(Optional) Delete the application once you're done using it <code>npx cdk destroy</code></p>
</li>
</ol>
<h2 id="heading-using-the-api">Using the API</h2>
<ol>
<li><p>Retrieve the following values of the Stack Outputs printed during the deployment in the previous step</p>
<ul>
<li><p><code>UrlShortenerCustomMetricsStack.APIKeyID</code> —&gt; You'll need to replace the string <code>&lt;api-key-id&gt;</code> with the value returned</p>
</li>
<li><p><code>UrlShortenerCustomMetricsStack.customAPIUrlOutput</code> —&gt; You'll need to replace the <code>&lt;your_url&gt;</code> with the value returned</p>
</li>
</ul>
</li>
<li><p>Retrieve the API Key value and use it to replace the <code>&lt;api-key-value&gt;</code> in the following examples, some options to do so would be:</p>
<ul>
<li><p>Navigate to the AWS Console and retrieve it manually</p>
</li>
<li><p>Execute the following AWS CLI command <code>aws apigateway get-api-key --api-key &lt;api-key-id&gt; --include-value --query "value" --output text</code></p>
</li>
</ul>
</li>
<li><p>Create your first redirection URL</p>
<pre><code class="lang-bash">     curl --location <span class="hljs-string">'https://&lt;your_url&gt;/exmaple'</span> \
     --header <span class="hljs-string">'Content-Type: application/json'</span> \
     --header <span class="hljs-string">'x-api-key: &lt;api-key-value&gt;'</span> \
     --data <span class="hljs-string">'{
         "originalURL": "https://lhidalgo.dev/url-shortener-custom-metrics",
         "ttlInSeconds": 360000
     }'</span>
</code></pre>
</li>
<li><p>The previous curl command will return the redirection URL as part of its body, the response should be <code>"https://&lt;your_url&gt;/exmaple"</code></p>
</li>
<li><p>Open that link on any browser and you'll be redirected to the URL you sent as <code>originalURL</code> in the previous request</p>
</li>
</ol>
<h1 id="heading-conclusion">Conclusion</h1>
<p>In conclusion, building a URL shortener with custom metrics using AWS services is easier than one could think of and allows you to self-host your solution giving you more flexibility into what statistics you want to create.</p>
<p>This article also showcases how easy it is to create custom CloudWatch metrics, which can be implemented in any existing project to increase the observability of the same and provide valuable insights into the behavior of your application and users.</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://github.com/Lorenzohidalgo/url-shortener-custom-metrics"><strong>Complete sample repository</strong></a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html"><strong>CloudWatch Embedded metric format</strong></a></p>
</li>
<li><p><a target="_blank" href="https://docs.powertools.aws.dev/lambda/typescript/latest/core/metrics/"><strong>Lambda PowerTools metrics package</strong></a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html"><strong>Metric Math expressions</strong></a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/hello_world.html#hello_world_bootstrap"><strong>CDK &amp; Bootstrapped AWS Account</strong></a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[DynamoDB Common Mistakes and how to fix them]]></title><description><![CDATA[TL;DRThis article provides a brief introduction to DynamoDB and the key differences between SQL and No-SQL databases in order to better understand the common mistakes and how to fix them. Feel free to skip to the Key takeaways section to read the mos...]]></description><link>https://lhidalgo.dev/dynamodb-common-mistakes-and-how-to-fix-them</link><guid isPermaLink="true">https://lhidalgo.dev/dynamodb-common-mistakes-and-how-to-fix-them</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[DynamoDB]]></category><category><![CDATA[NoSQL]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Tue, 29 Oct 2024 23:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434301311/ec714fb8-15a1-4ea3-8ad7-b88d231e1fa6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR</strong><br />This article provides a brief introduction to DynamoDB and the key differences between SQL and No-SQL databases in order to better understand the common mistakes and how to fix them. Feel free to skip to the Key takeaways section to read the most relevant points.</p>
<p>Not in the mood for reading? Check out the recorded version of this article on <a target="_blank" href="https://www.youtube.com/watch?v=p4K1L4kJcJs">YouTube</a></p>
</blockquote>
<h2 id="heading-introduction">Introduction</h2>
<p>Getting started with DynamoDB is fairly easy since there are a lot of examples and good documentation out there, but it’s as easy or even easier to end up using it wrong, due to a lack of knowledge.</p>
<p>During the last few years, the vast majority of mistakes I’ve seen have been due to misunderstandings or a lack of knowledge of how DynamoDB works and the differences between NoSQL and SQL databases.</p>
<p>This article aims to provide a brief introduction to DynamoDB and showcase the most common mistakes during its implementation and how one could solve them.</p>
<h2 id="heading-dynamodb-101">DynamoDB 101</h2>
<p>The key aspects of DynamoDB could be described as follows:</p>
<ul>
<li><p><strong>Managed NoSQL database by AWS:</strong> Designed to handle large volumes of data with low latency.</p>
</li>
<li><p><strong>High availability and automatic scalability:</strong> Automatically adjusts to demand without manual intervention (When configured to PAY_PER_REQUEST).</p>
</li>
<li><p><strong>Consistent millisecond performance:</strong> Ideal for applications that require fast response times.</p>
</li>
<li><p><strong>Supports flexible data models:</strong> Includes document and key-value tables, with secondary indexes for efficient queries.</p>
</li>
<li><p><strong>Global security and replication:</strong> Provides encryption and data replication across multiple regions for greater durability and availability.</p>
</li>
</ul>
<h2 id="heading-key-differences-sql-vs-nosql">Key differences: SQL vs NoSQL</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td>SQL</td><td>No-SQL</td></tr>
</thead>
<tbody>
<tr>
<td>Data Model</td><td>Relational - Tables with rows and columns</td><td>Documents, Key-value, …</td></tr>
<tr>
<td>Schema</td><td>Fixed Schema - requires definition</td><td>Flexible and Dynamic Schema</td></tr>
<tr>
<td>Queries</td><td>SQL</td><td>System Specific</td></tr>
<tr>
<td>Scalability</td><td>Vertical</td><td>Horizontal</td></tr>
<tr>
<td>Data Consistency</td><td>ACID (Atomicity, Consistency, Isolation, Durability)</td><td>Eventual consistency to improve availability and performance</td></tr>
</tbody>
</table>
</div><p>The table above shows what I personally would consider the key differences between both Database types since, if you’re not aware of them, you’ll probably end up using one of them wrong.</p>
<p>For example:</p>
<ul>
<li><p><strong>Schemas</strong> - No-SQL Databases don’t have a strict or predefined schema, meaning that you can store a mix of data types and structures in a single table.</p>
</li>
<li><p><strong>Data Consistency</strong> - In order to allow for high performance and availability, No-SQL databases are, in general, not capable of providing the data consistency we’re used to with SQL.</p>
</li>
</ul>
<p>Both of these examples are key things to keep in mind when choosing what database matches best your project, as No-SQL and DynamoDB in particular might not always be the right fit for you.</p>
<h3 id="heading-example-key-difference-sql-vs-dynamodb">Example Key Difference SQL vs DynamoDB</h3>
<p>A part of the differences in how they are built and how they work under the hood, it’s also important to consider the differences in how services can interact with the databases since, as stated above, No-SQL DBs tend to rely on system-specific query languages.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434386038/7131890d-1fab-4871-9a10-9927da32e76e.webp" alt class="image--center mx-auto" /></p>
<p>For example, <code>INSERT INTO</code> in SQL and <code>PutCommand</code> on DynamoDB will behave differently, and not knowing the default behaviors of the systems you interact with can land you in, what seems like inexplicable bugs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434459647/7655914b-b519-4154-b13f-d0c90bbe375f.webp" alt class="image--center mx-auto" /></p>
<p>What we mean by that is that the <code>PutCommand</code> and <code>UpdateCommand</code> on DynamoDB will, by default, behave like an <code>UPSERT</code> command in SQL.</p>
<p>This means that if developers are not careful enough or there is a lack of validations before performing the action, you might end up overwriting records (losing data!) or with downstream errors created by records without all the required attributes (records wrongly created by an <code>UpdateCommand</code>).</p>
<p>Avoiding this behavior can easily be done by adding conditions to those commands.</p>
<h2 id="heading-condition-expressions">Condition Expressions</h2>
<p>Condition Expressions could be seen as the DynamoDB implementation of the <code>WHERE</code> clause, but limited to be executed only over a single record.</p>
<h3 id="heading-introduction-1">Introduction</h3>
<p>Using them allows developers to specify what conditions should be met for a given action to be performed.</p>
<p>For example, if we want to avoid DynamoDB default UPSERT behavior on <code>PutCommand</code> and <code>UpdateCommand</code> it would be as easy as adding a condition to the command.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434497538/5c077523-ed77-4e05-b544-647e7cffbd79.webp" alt class="image--center mx-auto" /></p>
<p>In the above example, we added a <code>ConditionExpression</code> to the <code>UpdateCommand</code> to ensure that DynamoDB only updates records where the primary key exists (aka. it will only update existing records and fail if no record was found for the given keys).</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Any attribute names and values used inside a <code>ConditionExpression</code> will also be required to be specified under the <code>ExpressionAttributeNames</code> and <code>ExpressionAttributeValues</code>.</div>
</div>

<h3 id="heading-idempotency-checks">Idempotency Checks</h3>
<p>But the usage of Condition Expressions doesn’t stop there. It allows us to streamline for example the implementation of idempotency checks.</p>
<p>Idempotency checks are usually used to avoid processing a single event more than once. This becomes especially useful on distributed or event-driven applications where, in most cases, a single event could be delivered more than once or two consumers could be processing it at the same time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434615394/534a8f0c-5f9c-44e2-b5dd-6c6efb176b2a.webp" alt class="image--center mx-auto" /></p>
<p>The usual implementation of this kind of check is using a database or cache to store a record for every event that has been or is currently being processed.</p>
<p>This means that the first step for a consumer lambda would be to:</p>
<ol>
<li><p>Check if the Event has already been processed</p>
</li>
<li><p>Update the databases stating that the given event is currently being processed</p>
</li>
</ol>
<p>The screenshot above depicts what we would call a bad idempotency check implementation as it is being done with two different DynamoDB operations and we can’t ensure that this will stop concurrent processing of the same event, as there will be some time lapse between the read and the write operations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434641870/d33a0fa4-7ab4-42b1-9ab2-c16d6ce127b5.webp" alt class="image--center mx-auto" /></p>
<p>The correct implementation of an idempotency check using DynamoDB would be by using only one operation, either a <code>Put</code> or an <code>Update</code> command would work depending on if we need to persist any attributes stored by a previously failed execution.</p>
<p>The above sample implements a <code>PutCommand</code> with a condition expression that will only succeed if:</p>
<ul>
<li><code>attribute_not_exists(#pk)</code> - This will only be true if no record is found for the given primary key, as it’s a mandatory attribute</li>
</ul>
<p>OR</p>
<ul>
<li><code>attribute_exists(#pk) AND #status = :failedStatus</code> - This condition will only succeed if a record is found but the status of the previous execution was set as <code>FAILURE</code>. Implementing this additional check allows us to seamlessly retry failed executions.</li>
</ul>
<h3 id="heading-business-logic-checks">Business logic Checks</h3>
<p>Condition Expressions not only allow us to implement idempotency checks, but also allow us to implement a condition on any <code>Put</code> or <code>Update</code> command.</p>
<p>Another good scenario for it could be the backend of a given marketplace, where we would need to perform some business logic checks before a given transaction is approved.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434684039/b2d19e07-66ff-4fc9-b8f9-ad752334aea4.webp" alt class="image--center mx-auto" /></p>
<p>Similar to the previous scenario, developers might be tempted to implement the business logic as part of the code by making a read operation, performing any business logic, and finally making an update operation to store the final value.</p>
<p>This implementation would be flawed and probably generate some hard-to-find bugs in high-traffic scenarios, as no consistency can be ensured between the read and write operations and a wrong stock amount could be stored.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434723281/27a5c45c-2eaf-4867-8347-e6ee9b829608.webp" alt class="image--center mx-auto" /></p>
<p>A better approach to this would be using Condition Expressions and, if there is a requirement for specific error messages, adding the <code>ReturnValuesOnConditionCheckFailure</code> to <code>ALL_OLD</code>.</p>
<p>By configuring your DynamoDB request that way, DynamoDB will throw a <code>ConditionalCheckFailedException</code> if the <code>ConditionExpression</code> is not met and provide the record details as it were when the condition was analyzed.</p>
<p>Developers would be able to access the <code>error</code> and <code>error.Item</code> to run any additional logic and choose the appropriate error message, based on what part of the condition could have failed.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Do you want to learn more about DynamoDB Condition Expressions? Feel free to head over to <a target="_self" href="https://lhidalgo.dev/unlocking-the-power-of-dynamodb-condition-expressions">this article</a> covering them in more detail.</div>
</div>

<h2 id="heading-retrieving-data">Retrieving Data</h2>
<p>Similar to Put and Update operations, No-SQL DBs also present a different behavior regarding how developers are expected to handle the retrieval of data.</p>
<h3 id="heading-limitations">Limitations</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434798044/c27046ac-6496-478d-8255-c7dee8bb8074.webp" alt class="image--center mx-auto" /></p>
<p>The most known limitation is that No-SQL databases perform poorly with access patterns and queries over attributes not part of the keys.</p>
<p>A part of that, and similar to the <code>LIMIT</code> statement in SQL, Scan and Query operations can only retrieve up to a maximum of 1 MB of data at a time.</p>
<p>This presents a limitation for those cases where you need to retrieve more information or, especially, if you pass any type of query condition to your request, as DynamoDB will analyze up to 1 MB and could for example return an empty response even if matching records are present in the table.</p>
<h3 id="heading-pagination">Pagination</h3>
<p>To overcome that 1 MB limitation, developers are expected to implement pagination.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434841236/a376dccf-7d06-45ac-acf2-d171333e4efe.webp" alt class="image--center mx-auto" /></p>
<p>Implementing it is as easy as checking for the <code>LastEvaluatedKey</code> attribute in the <code>Scan</code> or <code>Query</code> response and passing it as the <code>ExclusiveStartKey</code> in the next request.</p>
<p>If this is developed as a recursive function, as shown in the above sample image, developers will ensure that all the records in a table or all records that match a specific query will be found and returned.</p>
<h2 id="heading-batch-operations">Batch Operations</h2>
<p><code>Put</code>, <code>Update</code>, <code>Query</code> and <code>Scan</code> operations are the most well-known operations, but there are also scenarios where there is a need of writing or reading multiple specific records at once.</p>
<h3 id="heading-limitations-1">Limitations</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434895736/572904c3-147a-4fd4-8259-118c68f641f0.webp" alt class="image--center mx-auto" /></p>
<p>For those scenarios, people usually think of implementing a loop or multiple requests in parallel, which triggers are usually not the best approach.</p>
<p>On one side, having a loop retrieving information is usually already flagged as a bad practice by linters with rules like <code>no-await-in-loop</code> from <a target="_blank" href="https://eslint.org/docs/latest/rules/no-await-in-loop">ESLint</a>.</p>
<p>This is due to the poor efficiency of having all those requests in sequence, which will increase the overall execution time of the function exponentially.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434919954/7714f499-c906-4508-92bd-e317127eed97.webp" alt class="image--center mx-auto" /></p>
<p>On the other side, developers might think that retrieving the data in parallel with a <code>Promise.all</code> or <code>Promise.allSettled</code> might be a good approach but this will also not scale well and be difficult to debug, as developers could face a maximum connection limit reached error.</p>
<p>The correct implementation would be to take advantage of the available Batch* operations of DynamoDB.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434942720/13fb42a8-fdac-430a-9625-0e08bca97951.webp" alt class="image--center mx-auto" /></p>
<p>There are two different batch operations that can be used with DynamoDB:</p>
<ul>
<li><p><code>BatchGetItems</code> - Operation that will allow us to retrieve up to 16 MB or 100 records from the same or different tables in a single request</p>
</li>
<li><p><code>BatchWriteItem</code> - As the name implies, this operation will allow us to write (<code>PutRequest</code>) but also to delete (<code>DeleteRequest</code>) up to 16 MB or 25 records to a single or multiple tables.</p>
</li>
</ul>
<p>These operations are especially useful for aggregating multiple <code>GetItem</code> or <code>Put/DeleteItem</code> requests into a single call to DynamoDB.</p>
<h3 id="heading-unprocessed-items">Unprocessed Items</h3>
<p>Similar to the Query and Scan operations and due to the 16 MB limit on the Batch* operations, developers should expect some requests to fail, either partially or entirely.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736434964228/8891303d-191e-43a9-8faf-284502da0cc1.webp" alt class="image--center mx-auto" /></p>
<p>Any request that doesn’t respect the 100 record read and 25 record write limit will fail entirely, throwing an error without doing any modifications on the DynamoDB tables.</p>
<p>The 16 MB limit is a bit trickier, as one could expect those requests to fail partially, DynamoDB will do its best to read or write up to a maximum of 16 MB for a single request and, if any records are not processed, it will return those as part of the <code>UnprocessedItems</code> attribute in the response.</p>
<p>Developers should always consider this when using these types of operations and implement a recursive function accordingly that will retry any <code>UnprocessedItems</code> found in the response.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Do you want to learn more about DynamoDB Batch Operations? Feel free to head over to <a target="_self" href="https://lhidalgo.dev/mastering-dynamodb-batch-operations-explained">this article</a> covering them in more detail.</div>
</div>

<h2 id="heading-conclusions">Conclusions</h2>
<p>DynamoDB is a powerful and versatile NoSQL database that offers unique advantages for various workloads. However, to fully leverage its capabilities, developers must understand its specific behaviors and limitations. By mastering concepts such as condition expressions, pagination, and batch operations, developers can create more efficient, consistent, and scalable applications.</p>
<p>Developers should consider the following key points when implementing any workload that relies on or interacts with DynamoDB.</p>
<h3 id="heading-key-takeaways">Key takeaways</h3>
<ul>
<li><p><strong>Put and Update act like UPSERTS:</strong> Using the PutItem or UpdateItem commands without adding any condition behaves like UPSERTS in SQL.</p>
</li>
<li><p><strong>Reducing calls with CONDITION EXPRESSIONS</strong>: Adding conditional expressions allows us to perform our logic in a single call and with data consistency.</p>
</li>
<li><p><strong>Query and Scan require paging logic</strong>: Query or Scan operations only operate on 1MB pages, paging is required to retrieve more information.</p>
</li>
<li><p><em>Use Batch</em> operations to reduce execution time*: A Batch* operation is more effective than multiple individual operations in sequence or parallel and can be used to aggregate operations to different tables into a single request.</p>
</li>
<li><p><strong>Use BatchGet to retrieve higher volumes of information</strong>: The BatchGet operation allows you to retrieve up to 16MB or 100 records compared to 1MB for Query or Scan operations.</p>
</li>
<li><p><strong>BatchGet and BatchWrite require retry logic</strong>: With Batch* operations it is essential to apply retry logic on UnprocessedItems.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Mastering DynamoDB: Batch Operations Explained]]></title><description><![CDATA[TL;DR; This article covers the usage of DynamoDB BatchWrite and BatchGet operations, and how implementing them can help you improve the efficiency by reducing the amount of requests needed in your workload.

Introduction
Have you ever developed any t...]]></description><link>https://lhidalgo.dev/mastering-dynamodb-batch-operations-explained</link><guid isPermaLink="true">https://lhidalgo.dev/mastering-dynamodb-batch-operations-explained</guid><category><![CDATA[DynamoDB]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[NoSQL]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Wed, 16 Oct 2024 12:46:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729082666386/cc0620fc-461d-487d-8c10-9268ec8fc8e2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR;</strong> This article covers the usage of DynamoDB BatchWrite and BatchGet operations, and how implementing them can help you improve the efficiency by reducing the amount of requests needed in your workload.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>Have you ever developed any type of workload that interacts with DynamoDB?</p>
<p>If so, you probably have encountered the requirement of retrieving or inserting multiple specific records, be it from a single or various DynamoDB tables.</p>
<p>This article aims to provide you with it by providing all the required resources and knowledge to implement the usage of DynamoDB batch operations and, as a bonus point, increase the efficiency of your current workloads.</p>
<h1 id="heading-what-are-batch-operations">What are Batch Operations?</h1>
<h2 id="heading-introduction-1">Introduction</h2>
<p>When talking about batch operations or batch processing we refer to the action of aggregating a set of instructions in a single request for them to be executed all at once. In terms of interacting with DynamoDB, we could see it as sending a single request that would allow us to retrieve or insert multiple records at once.</p>
<h2 id="heading-common-bad-practices">Common Bad practices</h2>
<p>Continuing with the sample situation mentioned in the introduction, you may face the requirement of having to retrieve or store multiple records at once.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729070560255/056bf226-3346-4508-a8ba-4c888bf6a2fd.webp" alt="Code snippet with a &quot;for&quot; loop iterating over &quot;items,&quot; using &quot;await getItem(keys)&quot; inside the loop." class="image--center mx-auto" /></p>
<p>For that scenario, most junior developers might rely on looping over a set of keys and sending the <code>GetItem</code> requests in sequence or a mid-level developer might propose to parallelize all those requests using for example a <code>Promise.all</code>, but both approaches are flawed and won’t scale well.</p>
<p>On one side, the <code>for-loop</code> will even be detected by some linters (with rules like <code>no-await-in-loop</code>) as this implementation would increase the execution time exponentially.</p>
<p>On the other side, the <code>Promise.all</code> approach will be a tad more efficient by parallelizing the requests, but with high workloads, developers would end up facing issues like the maximum connection limit reached error.</p>
<h2 id="heading-recommended-implementation">Recommended Implementation</h2>
<p>Now that we have gone over some bad practices in implementing it and that you have probably thought of a few projects that could be improved, we’ll dive into how we can take the most advantage of it.</p>
<p>DynamoDB offers two different types of operations <code>BatchGetItem</code> and <code>BatchWrtieItem</code> which we will take a look into as part of this article.</p>
<p>There is also <code>BatchExecuteStatement</code> for those using PartiQL, but we will leave that one for a future article to cover PartiQL in detail.</p>
<h3 id="heading-batchgetitem">BatchGetItem</h3>
<p>This operation type will allow us to aggregate up to the equivalent of 100 GetItem requests in a single request.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729075114071/73a54a29-5f41-4765-9a15-9a39587418e6.png" alt="Code snippet showing a `BatchGetCommand` function for fetching items from two tables, &quot;Table 1&quot; and &quot;Table 2,&quot; using primary keys (PK) and sort keys (SK)." class="image--center mx-auto" /></p>
<p>Meaning that with this operation we could retrieve up to 100 records or 16 MB from a single or multiple table at once.</p>
<h3 id="heading-batchwriteitem">BatchWriteItem</h3>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">PutRequests will overwrite any existing records with the provided keys.</div>
</div>

<p>This operation, even if it only contains <code>write</code> as part of its name, will allow us to aggregate up to 25 <code>PutItem</code> and <code>DeleteItem</code> operations in a single request.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729075540123/1bec103b-6c0a-4d7c-aaf8-c873560bcc08.png" alt="Screenshot of a JavaScript code snippet for a `BatchWriteCommand`. It includes request items for two tables, &quot;Table 1&quot; with a `PutRequest`, and &quot;Table 2&quot; with a `DeleteRequest` using a primary key." class="image--center mx-auto" /></p>
<p>Similar to the previous option, we’ll still be limited by the 16 MB maximum, but we would theoretically be able to replace 25 sequential or parallel requests with a single one.</p>
<h3 id="heading-pagination-for-batch-operations">Pagination for Batch operations</h3>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Pagination is only valid for the 16 MB limit if the requests don’t follow the 100 record read or the 25 record write limit DynamoDB will throw a <code>ValidationException</code> instead.</div>
</div>

<p>Similar to the <code>Scan</code> and <code>Query</code> operations, using any of the above <code>Batch*Item</code> operations can incur in the scenario where the 16 MB maximum is reached and some type of pagination is required.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729075671379/880aa329-d1d8-4caa-a9cb-455d630f86d9.png" alt="Screenshot of a JavaScript code snippet defining an asynchronous function named `executeRequest`. It uses a try-catch block to handle a `payload`, checking for `UnprocessedItems`. If any, it recursively calls itself with a `BatchWriteItemCommand`. Errors are logged to the console." class="image--center mx-auto" /></p>
<p>For Batch* operations this comes in the form of the <code>UnprocessedKeys</code> attribute that can be part of the response.</p>
<p>Developers are expected to check for this attribute in the response and, if desired, implement its usage as a recursive function to process them automatically.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Full examples for Retrieving, Inserting, and Deleting records using BatchOperations with a recursive implementation to automatically handle the <code>UnprocessedKeys</code> can be found <a target="_blank" href="https://github.com/Lorenzohidalgo/dynamodb-batch-samples/tree/main/samples">here</a>.</div>
</div>

<h1 id="heading-real-world-use-cases">Real-world Use Cases</h1>
<p>Now that we are aware of all options and limitations regarding how we can process records in batch in DynamoDB, let’s see some scenarios that will showcase some real-life improvements.</p>
<h2 id="heading-scenario-1-retrieving-data-from-multi-table-design-architecture">Scenario 1: Retrieving Data from Multi-table Design Architecture</h2>
<p>For this first scenario, let’s imagine we are looking to improve the performance of a REST API that, given an array of <code>productId</code>, will return us the list of desired product details with their respective stock and exact warehouse location. The data is stored in multiple tables, one for each data model (products, stock tracking, and warehouse product location).</p>
<h3 id="heading-before">Before</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729077421343/2a640357-f0fc-4adf-b8c9-b18cea71eb2b.png" alt="JavaScript code snippet that retrieves product, stock, and location data for a list of product IDs and returns them in an array." class="image--center mx-auto" /></p>
<p>The initial implementation was developed by having a for-loop to go over all the provided <code>productIds</code> and sequentially retrieve all the required data from the different tables.</p>
<h3 id="heading-after">After</h3>
<p>From that initial implementation, you should be able to detect two distinct flaws:</p>
<ul>
<li><p><code>no-await-in-loop</code> - There is a loop with asynchronous operations inside, which is usually a bad practice, as all operations for a given operation will need to be completed before the next one can start.</p>
</li>
<li><p>Sequential <code>await getItem</code> requests - This is also a bad practice, as the three operations are independent from each other and we’d ideally not want for them to be blocked by each other.</p>
</li>
</ul>
<p>A better approach would look something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729080387196/da91368d-0ad5-4454-ac14-0220d241ae49.png" alt="A code snippet with four steps: 1) Checks if `idList` has more than 33 items and throws an error if true. 2) Builds a payload with `buildPayload(idList)`. 3) Awaits a recursive batch get with `recursiveBatchGet(payload)`. 4) Maps the responses to products with `mapResponse(batchGetResponses)` and returns them." class="image--center mx-auto" /></p>
<ol>
<li><p><strong>Input Validation</strong> - Set a limit of maximum items to be requested to avoid requiring parallel <code>BatchGetItem</code> requests.<br /> For example - max. 100 items per <code>BatchGetItem</code> request and every product requires 3 <code>GetItem</code> requests means that a single <code>BatchGetItem</code> request can retrieve up to 33 product details.</p>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">⚠</div>
 <div data-node-type="callout-text">This step could be avoided and execute <code>BatchGetItem</code> requests in parallel, but there could be a chance of facing issues like the maximum connection limit reached error.</div>
 </div>
</li>
<li><p><strong>Build Payloads</strong> - a helper function will be needed to programmatically build the required payload for the <code>BatchGetItem</code> operations taking into consideration the different tables that need to be accessed for each product ID.</p>
</li>
<li><p><strong>Recursive</strong> <code>BatchGetItem</code> - a helper function that recursively calls itself to ensure that all <code>UnprocessedKeys</code> are retried.</p>
</li>
<li><p><strong>Response parsing</strong> - a helper function that transforms the <code>BatchGetItem</code> response to the given schema that the consumers are expecting for this API</p>
</li>
</ol>
<p>Applying all these changes should significantly increase the efficiency and performance of the API.</p>
<h2 id="heading-scenario-2-inserting-data-in-a-single-table-design-architecture">Scenario 2: Inserting Data in a Single-table Design Architecture</h2>
<p>The second scenario would imply a DynamoDB single table design architecture where we have a single table to store all the information needed for a Dashboard to analyze racehorses’ historical data. Records such as basic horse information, performance statistics, and race results are stored in the same table.</p>
<h3 id="heading-before-1">Before</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729081347367/b2f4fbbe-8a34-4bd2-a835-f0ee191bc14e.png" alt="Code snippet for storing horse details, statistics, and race information using the `putItem` function in an asynchronous manner." class="image--center mx-auto" /></p>
<p>Similar to the first scenario, we can see that the initial implementation is based on a set of sequential <code>PutItem</code> requests.</p>
<h3 id="heading-after-1">After</h3>
<p>From that initial implementation, you should be able to detect two distinct flaws:</p>
<ul>
<li><p><code>no-await-in-loop</code> - There is a loop with asynchronous operations inside, which is usually a bad practice, as all operations for a given operation will need to be completed before the next one can start.</p>
</li>
<li><p>Sequential <code>await putItem</code> requests - This is also a bad practice, as the three operations are independent from each other and we’d ideally not want for them to be blocked by each other.</p>
</li>
</ul>
<p>A better approach would look something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729081563817/7769873b-623b-4a40-87e8-09e8d8ea19bd.png" alt="Code snippet showing two steps: 1. Building a payload with the function `buildPayload` using parameters `horse`, `stats`, and `races`.2. Performing a recursive batch write with the function `recursiveBatchWrite`, using the payload." class="image--center mx-auto" /></p>
<ol>
<li><p><strong>Build Payloads</strong> - a helper function will be needed to programmatically build the required payload for the <code>BatchGetItem</code> operations taking into consideration the different tables that need to be accessed for each product ID.</p>
</li>
<li><p><strong>Recursive</strong> <code>BatchWriteItem</code> - a helper function that recursively calls itself to ensure that all <code>UnprocessedKeys</code> are retried.</p>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">💡</div>
 <div data-node-type="callout-text">This approach would only work to upload up to 25 records for a single horse.</div>
 </div>


</li>
</ol>
<p>Applying all these changes should significantly reduce the required time to upload all information.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Utilizing batch operations in DynamoDB is a powerful strategy to optimize your database interactions. By aggregating multiple requests into a single operation, you can improve performance, reduce latency, and manage resources more effectively. Whether you're dealing with multi-table architectures or single-table designs, batch operations offer a scalable solution to handle large volumes of data efficiently. As you continue to work with DynamoDB, consider integrating batch operations into your workflows to maximize the potential of your applications.</p>
<h2 id="heading-recap-of-key-points">Recap of key points</h2>
<ul>
<li><p>BatchGetItem can retrieve up to 100 records or 16 MB of data in a single request.</p>
</li>
<li><p>BatchWriteItem can be used to insert or delete up to 25 records or 16 MB of data in a single request.</p>
</li>
<li><p>Using Batch* operations can help you reduce the execution time considerably by aggregating requests that were currently being done in sequence.</p>
</li>
</ul>
<h2 id="heading-additional-resources-and-references">Additional resources and references</h2>
<ul>
<li><p><a target="_blank" href="https://github.com/Lorenzohidalgo/dynamodb-batch-samples">https://github.com/Lorenzohidalgo/dynamodb-batch-samples</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html">https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html">https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchExecuteStatement.html">https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchExecuteStatement.html</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[[Webinar] DynamoDB - Common mistakes and how to optimize usage]]></title><description><![CDATA[https://www.youtube.com/watch?v=p4K1L4kJcJs
 
This webinar briefly introduces DynamoDB, the most relevant differences between SQL and NoSQL Databases, the most common mistakes when interacting with DynamoDB, and how to solve them.]]></description><link>https://lhidalgo.dev/webinar-dynamodb-common-mistakes-and-how-to-optimize-usage</link><guid isPermaLink="true">https://lhidalgo.dev/webinar-dynamodb-common-mistakes-and-how-to-optimize-usage</guid><category><![CDATA[DynamoDB]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[#howtos]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Thu, 01 Aug 2024 08:03:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722499330481/58678e4e-d77e-4acf-b76f-4c142504782b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=p4K1L4kJcJs">https://www.youtube.com/watch?v=p4K1L4kJcJs</a></div>
<p> </p>
<p>This webinar briefly introduces DynamoDB, the most relevant differences between SQL and NoSQL Databases, the most common mistakes when interacting with DynamoDB, and how to solve them.</p>
]]></content:encoded></item><item><title><![CDATA[[Video] What does a Software Engineer in the Cloud actually do? | Working in the cloud ☁️]]></title><description><![CDATA[https://www.youtube.com/watch?v=0EXGfdt_ZqQ
 
Video Description
Three cloud software engineers, Danielle Heberling, Ian McKay, and Lorenzo Hidalgo, provide an in-depth look at their roles and responsibilities, key tasks, and the challenges they face ...]]></description><link>https://lhidalgo.dev/video-what-does-a-software-engineer-in-the-cloud-actually-do-working-in-the-cloud</link><guid isPermaLink="true">https://lhidalgo.dev/video-what-does-a-software-engineer-in-the-cloud-actually-do-working-in-the-cloud</guid><category><![CDATA[Cloud]]></category><category><![CDATA[video]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Wed, 15 May 2024 22:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1718969638150/80e52828-adc0-48ae-ac97-446cb758bfa7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=0EXGfdt_ZqQ">https://www.youtube.com/watch?v=0EXGfdt_ZqQ</a></div>
<p> </p>
<h2 id="heading-video-description">Video Description</h2>
<p>Three cloud software engineers, Danielle Heberling, Ian McKay, and Lorenzo Hidalgo, provide an in-depth look at their roles and responsibilities, key tasks, and the challenges they face working in cloud computing.</p>
<p>📜Table of contents:<br /><a target="_blank" href="https://www.youtube.com/watch?v=0EXGfdt_ZqQ&amp;t=0s">00:00</a> Meet the Cloud Experts: Introductions<br /><a target="_blank" href="https://www.youtube.com/watch?v=0EXGfdt_ZqQ&amp;t=12s">00:12</a> What Does a Cloud Software Engineer Do?<br /><a target="_blank" href="https://www.youtube.com/watch?v=0EXGfdt_ZqQ&amp;t=45s">00:45</a> The Role of a Cloud Architect<br /><a target="_blank" href="https://www.youtube.com/watch?v=0EXGfdt_ZqQ&amp;t=102s">01:42</a> A Day in the Life of a Technical Architect<br /><a target="_blank" href="https://www.youtube.com/watch?v=0EXGfdt_ZqQ&amp;t=236s">03:56</a> Software Engineer's Daily Routine<br /><a target="_blank" href="https://www.youtube.com/watch?v=0EXGfdt_ZqQ&amp;t=437s">07:17</a> The Importance of Meetings in Cloud Engineering<br /><a target="_blank" href="https://www.youtube.com/watch?v=0EXGfdt_ZqQ&amp;t=609s">10:09</a> Challenges in Cloud Engineering<br /><a target="_blank" href="https://www.youtube.com/watch?v=0EXGfdt_ZqQ&amp;t=853s">14:13</a> Dispelling Common Cloud Misconceptions</p>
<p>This video is part of a series called "Working in the cloud ☁️". Where cloud computing software engineers talk about how is to work in the cloud, getting a job in this space, and advancing their career.</p>
<p>Check the whole playlist here: <a target="_blank" href="https://www.youtube.com/playlist?list=PLGyRwGktEFqdKC9YHnENL1gp8iQYM4L58">Working in the cloud ☁️</a></p>
<p>🧑🏽‍💻 Find our guests online:<br />- Danielle Heberling - Senior Software Engineer at Ensomata<br />Personal website: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqa3hsN3pUallvcm9DSEtrU3VjVC1YVVQzZ2VBQXxBQ3Jtc0ttREU4SXV1N0ZwbEJ4VjNPRUZjcmNYNzFibWp4Z3A2XzZsQ25iVTViUXFyY2xENV9WcnppLUJzTEJ6eDZfSHdwQmU5SUpYclFpSXdtNG92NlNacFUxdWNmMk1OWmZVMGg4RGV4RjFrb2JJNlByZ19lMA&amp;q=https%3A%2F%2Fdanielleheberling.xyz%2F&amp;v=0EXGfdt_ZqQ">https://danielleheberling.xyz/</a><br />X: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqa3h4YmI3d2VXaWZ4NHJXdmtqc09fLVVPdGJqUXxBQ3Jtc0tucFJJS3RlZkQtbkVlQXNnTXVCUmtTRzZMaW1CbzF4LTJzU1VPdUh4Y1Q2VnF1YjUxTkdUM1hKUzJhYV9XRkhzczdlcmJ0a0l5OVpFd0x3cElBWDlzcXhRZUpoTDA0QS1ZYS1MNFVSOHJzN1pFNS1idw&amp;q=https%3A%2F%2Ftwitter.com%2Fdeeheber&amp;v=0EXGfdt_ZqQ">/ deeheber</a><br />Github: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbjl4NjBEdEZzUFVaeXFyd3lWcXBnV2JvdGFHd3xBQ3Jtc0tuM0dpOUstN0NvS0FYdDhDckhORW81NXFvZEJLYzFwZ213M3RsSWxoSWJubWZOSnpaS0RsNktxYi11MmdtWUZrY2ZUMWpfNjliOWRqZmotWW50eWtOMk51R3B4YXhQbVkyTlZPbVVid2NTcWszLXN5aw&amp;q=https%3A%2F%2Fgithub.com%2Fdeeheber&amp;v=0EXGfdt_ZqQ">https://github.com/deeheber</a><br />LinkedIn: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbk85TFp0YzE4RlpwX0J1T21fYWlmTTJnbUNPZ3xBQ3Jtc0trc3pfQ3I1Q1RLT3dpRkROX2lGUjJ4YlNacnA2a3RIaXVza0NtQXFqOUQ1QTNscnZ1RjQxTWZCemVBd1lfaFNJcUlCZTBjT0hsWUpqd2RBS18weVlhLTh2dGd0SmxKYmN1NHZDQjRjMWhnbC14bzdGMA&amp;q=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fdeeheber%2F&amp;v=0EXGfdt_ZqQ">/ deeheber</a><br />Dev.to: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqa1VmVmtIQWJfaHIzaEhOb2VQTWVOdDBCMjhtUXxBQ3Jtc0tra05hN1hacGFuSUJzYTVMSWE2UXNjNFdjb0ZUYmRuM1dScFhIV296YTNrbWNVU050cW1fVmtVeThEY09rcGdiRElSTXNGaWEwc3VNRHNxVk1ZLVljYVNtSUxoUFNhUDNjSjBndUlyM0RpS3l5Tll5dw&amp;q=https%3A%2F%2Fdev.to%2Fdeeheber&amp;v=0EXGfdt_ZqQ">https://dev.to/deeheber</a><br />- Ian Mckay - Cloud Principal at Kablamo<br />X: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbDYxV243OVZhN3RGTXVQUEJFN25CWlBLaWh3QXxBQ3Jtc0tsQllTLWxOZkxxYkJiZDJNbVFVWVJuZ083a1E2N09FS1JTXy1jRG40bVJ4NU5OSjdpOGI1SG1sSU4wSVQxSFNVaXVTSTBkbzJ6UlE3Y3dWYUw4Z0cyQVRFVGkyNHN0SGFhTWRxY3pYZkotRkZOenBlMA&amp;q=https%3A%2F%2Ftwitter.com%2Fiann0036&amp;v=0EXGfdt_ZqQ">/ iann0036</a><br />LinkedIn: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbHVtaDZUVEJpR0VrVGpYU0VjaFpGWGVDSkpBUXxBQ3Jtc0tuZExaMkZJbjVGVFdZeEdsbk1oWEJUZmRzOTBEWTNEenJHVXhXckViNDNHUmNxNmVBeHlfbHhXUE5QenJQMWhET3k2UjB4aThtNDByM3JDWmhNbzhHbURyTlVDOUdtUV82dDZqcjI1Sm5PcnU1Ykwwbw&amp;q=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fiann0036%2F&amp;v=0EXGfdt_ZqQ">/ iann0036</a><br />Blog: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbXRKdDJ0bzhWQVA2S1pldy13UXRodkdBSjZ6d3xBQ3Jtc0tsMU9LbjJrQXdBUmF1TzdQUWQ3NWJHRExHSm1SS3JiLUNDZk8zUmE3Y0Z3MzR0ZVhjYnJyTUZOcVc4SEhFR0JnTWZBN2h3MGFvOW9NYmxBZDhWZUZ4OXhRS0d2bWlaRmtRNXpTQ0hnNmsxT3VjeFNpQQ&amp;q=https%3A%2F%2Fonecloudplease.com%2F&amp;v=0EXGfdt_ZqQ">https://onecloudplease.com/</a><br />- Lorenzo Hidalgo Gadea - Independent Contractor | Staff Engineer at Serverless Guru<br />Linkedin: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqa2U0eTRkdmUySmJqSi0yMjF1WDlVTlM4VDNqd3xBQ3Jtc0ttVEZLczBKMjdlX2RuWmI5ZVNZdVZSM09lMzl2LU56dURNenF2anp3LVUydERoWFJpa09NSlUtRC00MlNkOXFDNXRkWDg4NUdCNndsbzJyNm1ZNXBneEZsSW9sWk8ybUhMcUg2eUhyZ1NfX3U1OUtFVQ&amp;q=https%3A%2F%2Fwww.linkedin.com%2Fin%2Florenzo-hidalgo-gadea%2F&amp;v=0EXGfdt_ZqQ">/ lorenzo-hidalgo-gadea</a><br />Blog: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqa3pzZVd3WDMzUGcwT2VTV1A2MXpKOUVZN2FPQXxBQ3Jtc0trRnB2NzV5b1JGTG9PWWdST1VoS19pZTJwRXFGX3UwTy1uWWdOaG9rS3ozdnJLc0dSeC1peld1V2hvVjBYUkRQVXVSbDU3OURZc2FpNGxPaHdCM056SGVCdktzYzBDUnluamxmYWlCX2JfNWZJT05CQQ&amp;q=https%3A%2F%2Flhidalgo.dev%2F&amp;v=0EXGfdt_ZqQ">https://lhidalgo.dev/</a><br />X: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbThLRFN6dUZ4TXpmZ1R5ZFFpU0FiT0JGNE9ZUXxBQ3Jtc0tsbnpELV9GRnJQVkVrQmFsTEcydG1TT1lmaWh5NGd4QVEtVEFQR09UVjNDX2hEd1lDbkwyOUI0S2J5RGx0RXhRN194VElLbEp6LUJCcDRJZnRHZnZQRnRycVhOcEgzcVlGMkhkVjVpYk9CY0l5Rnpocw&amp;q=https%3A%2F%2Ftwitter.com%2Flhidalgo_dev&amp;v=0EXGfdt_ZqQ">/ lhidalgo_dev</a></p>
<h2 id="heading-credits">Credits</h2>
<p>Video From Marcia Villalba, follow her on:<br />🐦 Twitter: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbWVBbE95aHREcDVTRkxzUUdxSHRYQW9JWEpmd3xBQ3Jtc0ttWVNQQXlYTHl1bGJwTVJkRWI1bWs2SHUtR01aSzQ4cGZMSXlIS1kyYURDQ0d6SGRFX096c2NhZWF6SkZ3YkZPel9vcS01VnlYOVA5bC13OG44YWVuYzl0NGV3WmhKeFVCOVFHNlIyalVEc3dCYjltNA&amp;q=https%3A%2F%2Ftwitter.com%2Fmavi888uy&amp;v=0EXGfdt_ZqQ">/ mavi888uy</a><br />🖇️ Linkedin: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbVpDYndtNWU3NnFaRW0tT0haai1kRUVEdjRDd3xBQ3Jtc0tuWllGSUdrd1ZsdkNEa0kxclRrWUdzYUlaZS1GVk9yOXE2cy1JVk9QWXdDeTVpdW9BU1hEUjdvSkVyNXlqYVVoTWl6Y2JTcFNlNWpocEJSc1VRQjRwekNpT2Z1cnFBN29iY00zSTU4d3lsT0pkZDBsUQ&amp;q=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fmarciavillalba%2F&amp;v=0EXGfdt_ZqQ">/ marciavillalba</a><br />📧 Newsletter: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbDNFb1FoTkF2QzFOV3FIVGMyT1IyUlp3eEpqd3xBQ3Jtc0trR01jVHZDcGhoTVUzTGcxdVdLM0RRWV9lQ1Y1NVQzb1JHV3pjU3lzdmNvQWRFWm5Ob0JlOURlcGdzejVRdTNjTHRXbzlKLWRQekJOeEx2dHU2YmJkZm5odnA0MmRNOEVGVlloTnpNVnJ4Z280YkxOaw&amp;q=https%3A%2F%2Fmarciavillalba.substack.com%2F&amp;v=0EXGfdt_ZqQ">https://marciavillalba.substack.com/</a><br />📺 AWS Spanish Youtube Channel: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbUxlY1RYel93ZHpGcE56MjdhSy14bGk1djcwZ3xBQ3Jtc0trdGlCZGhueTZfRDZlVEtINzVjQ1lrdVlYc3V2UGw4SnN0QXpNblcxTUpnblZZT29LSjh5OHNGM2lmam9raTZJalBPdGdNLVZ1MThkX2pmeVZMWjF3X2FXcndxclBHelJIRjUyLV9YTTdMSV95YlpWdw&amp;q=https%3A%2F%2Fbit.ly%2Faws-esp-yt&amp;v=0EXGfdt_ZqQ">https://bit.ly/aws-esp-yt</a><br />📷 Instagram: foobar_codes<br />📚 All my Serverless Courses: <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbUx4bGQ3Yzk4dG9kVzkxZHFBd0ZzZU5ic3o1Z3xBQ3Jtc0tsWUx1UmJiTTZnRC1KQVNSTXpGQTFVOTEzR3g0b21kV0p4WVNMX2JwRXMtdzJFRVBRNFhDMFladzNEaWhMSkxNMkdwY2JIUlRmY0liR05nZjhWdXNpeDJWMnF5SnZ6bWQyNlNfQ0xMTElwNEFsWnhSNA&amp;q=https%3A%2F%2Fmarcia.dev%2Fcourses%2F&amp;v=0EXGfdt_ZqQ">https://marcia.dev/courses/</a><br />✍️ My blog - <a target="_blank" href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqa3JJc2xuX2xiVkZfdS1rMjJrbnFDWmtKSmhYUXxBQ3Jtc0tuQXptZFFmVGdrRWFBZUtJZFYxMy1aTjRQYWFzNTFFZUVLZldLdlV2d1FSaFd6WVZGS0Z5MlUwZ3Z4N19KT1Vfb09oMjNQcHRGT1pFZWw2azQ0aVBEMW9zX1EwM3pPRlhXM3gzbHo0OWJPUGppbjVuMA&amp;q=https%3A%2F%2Fblog.marcia.dev%2F&amp;v=0EXGfdt_ZqQ">https://blog.marcia.dev</a></p>
]]></content:encoded></item><item><title><![CDATA[Enhance Your AppSync API Development: Easy Steps to Auto-Generate Postman Collections]]></title><description><![CDATA[TL;DR;This article covers how developers can take advantage of the serverless-gql-generator plugin for Serverless Framework to automatically generate new Postman Collections or Raw Requests for an AppSync API.

Introduction
A few weeks ago, I announc...]]></description><link>https://lhidalgo.dev/enhance-your-appsync-api-development-easy-steps-to-auto-generate-postman-collections</link><guid isPermaLink="true">https://lhidalgo.dev/enhance-your-appsync-api-development-easy-steps-to-auto-generate-postman-collections</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless framework]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[GraphQL]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Fri, 15 Mar 2024 11:32:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710502222042/e1b58263-bbc9-444e-997c-b308884392a0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>TL;DR;<br />This article covers how developers can take advantage of the <code>serverless-gql-generator</code> plugin for Serverless Framework to automatically generate new Postman Collections or Raw Requests for an AppSync API.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>A few weeks ago, I announced the release of a new Serverless Framework Plugin that I had been working on called <a target="_blank" href="https://www.npmjs.com/package/serverless-gql-generator">serverless-gql-generator</a>.</p>
<p>The idea for this plugin came from the difficulties faced during one of my projects where it was too time-consuming for developers to keep the sample requests and Postman Collection for every new schema update. This pain point was further intensified when we started to develop multiple interdependent AppSync APIs simultaneously, requiring multiple Postman Collections to be updated and shared across teams.</p>
<p>In this article, we will cover how to integrate the Serverless Framework plugin into your development flow and CICD pipelines.</p>
<h1 id="heading-main-plugin-features">Main Plugin Features</h1>
<p>As of writing this article, the current version <code>1.2.1</code> of the plugin has the following features:</p>
<ul>
<li><p><strong>Automatic GraphQL Request Generation</strong> - The plugin will generate new requests on demand or during deployment, ensuring they are always up to date with the latest schema version.</p>
</li>
<li><p><strong>Automatic URL &amp; API Key Retrieval</strong> - The URL and API Key will be automatically populated with the latest AppSync configuration.</p>
</li>
<li><p><strong>Choose between Inline or using variable file input</strong> - Developers can configure the plugin to generate requests with Inline input or with a separate file for variable input.</p>
</li>
<li><p><strong>Exports requests to independent Files and Postman Collections</strong> - Depending on the use case, developers can configure the plugin to export the generated requests to independent <code>.graphql</code> files, a Postman Collect or both.</p>
</li>
<li><p><strong>Upload the generated files to S3</strong> - As the icing on the cake, this plugin also automates the upload of the generated files to the configured S3 bucket.</p>
</li>
</ul>
<h1 id="heading-prerequisites"><strong>Prerequisites</strong></h1>
<p>In order to be able to implement this plugin into your workflow you will at least need:</p>
<ul>
<li><p>Node JS installation</p>
</li>
<li><p>AWS Account to deploy the API</p>
</li>
<li><p>A project that uses the Serverless Framework to deploy an AppSync API - examples will be shown using <a target="_blank" href="https://github.com/Lorenzohidalgo/appsync-decoupling-sample">this repository</a></p>
</li>
<li><p><a target="_blank" href="https://www.postman.com/downloads/">Postman</a>, <a target="_blank" href="https://graphbolt.dev/">GraphBolt</a> or any other tool to send requests and test the generated requests</p>
</li>
</ul>
<h1 id="heading-installation-process">Installation Process</h1>
<p>Installing and adding the plugin to your project is fairly straightforward to accomplish by following two simple steps.</p>
<h2 id="heading-install-the-plugin-using-npm">Install the plugin using NPM</h2>
<p>Use the following command to install the plugin and save it as a <code>devDependency</code> in your project:</p>
<pre><code class="lang-javascript">npm install -D serverless-gql-generator
</code></pre>
<h2 id="heading-add-the-plugin-to-your-project">Add the Plugin to your project</h2>
<p>Add the plugin under the <code>plugins</code> list in your <code>serverless.yml</code> file</p>
<pre><code class="lang-yaml"><span class="hljs-attr">service:</span> <span class="hljs-string">my-app</span>

<span class="hljs-attr">plugins:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">serverless-gql-generator</span>
</code></pre>
<h1 id="heading-using-the-plugin">Using the plugin</h1>
<p>After adding the plugin to the <code>plugins</code> list, it will start generating the Postman Collections every time the service is deployed.</p>
<p>The experience can be improved further by configuring the plugin to match your needs better and by using the CLI commands to enable the request generation without the need to redeploy your service.</p>
<h2 id="heading-overriding-the-default-behaviour">Overriding the default behaviour</h2>
<p>The plugin's default behaviour is to generate (and save locally under the <code>./output/</code> folder) a Postman Collection.</p>
<p>This behaviour can be configured by overriding the defaults and adding any of the configuration attributes:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">service:</span> <span class="hljs-string">my-app</span>

<span class="hljs-attr">plugins:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">serverless-gql-generator</span>

<span class="hljs-attr">gql-generator:</span>
  <span class="hljs-attr">schema:</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">./schema.graphql</span> <span class="hljs-comment"># Overrides default schema path </span>
    <span class="hljs-attr">encoding:</span> <span class="hljs-string">utf-8</span>
    <span class="hljs-attr">assumeValidSDL:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">output:</span>
    <span class="hljs-attr">directory:</span> <span class="hljs-string">./output</span> <span class="hljs-comment"># Output directory</span>
    <span class="hljs-attr">s3:</span> <span class="hljs-comment"># Enables Upload to AWS S3</span>
      <span class="hljs-attr">bucketName:</span> <span class="hljs-string">gql-output-bucket</span> <span class="hljs-comment"># Mandatory Bucket name</span>
      <span class="hljs-attr">folderPath:</span> <span class="hljs-string">s3folder/path</span> <span class="hljs-comment"># Override Folder Path inside s3, defaults to `${service}/${stage}`</span>
      <span class="hljs-attr">skipLocalSaving:</span> <span class="hljs-literal">false</span> <span class="hljs-comment"># if the files should also be saved locally or not</span>
    <span class="hljs-attr">useVariables:</span> <span class="hljs-literal">true</span> <span class="hljs-comment"># use variables or have the input inline</span>
    <span class="hljs-attr">maxDepth:</span> <span class="hljs-number">10</span> <span class="hljs-comment"># max depth for schema recursion</span>
    <span class="hljs-attr">rawRequests:</span> <span class="hljs-literal">false</span> <span class="hljs-comment"># set to true to generate raw requests</span>
    <span class="hljs-attr">postman:</span>
      <span class="hljs-attr">collectionName:</span> <span class="hljs-string">test-name</span> <span class="hljs-comment"># Overrides colection name, defaults to `${service}-${stage}`</span>
      <span class="hljs-attr">url:</span> <span class="hljs-string">https://test.com/graphql</span> <span class="hljs-comment"># Overrides url for postman collection</span>
      <span class="hljs-attr">apiKey:</span> <span class="hljs-string">abc-123</span> <span class="hljs-comment"># Overrides default API Key if any</span>
</code></pre>
<h3 id="heading-default-schema-options">Default Schema options</h3>
<p>The plugin expects the schema to be in the root folder of the project and be called <code>./schema.graphql</code>, Developers can update the configuration under <code>gql-generator.schema</code> in order to update the <code>path</code> or <code>encoding</code> of their schema.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">gql-generator:</span>
  <span class="hljs-attr">schema:</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">./schema.graphql</span> 
    <span class="hljs-attr">encoding:</span> <span class="hljs-string">utf-8</span>
    <span class="hljs-attr">assumeValidSDL:</span> <span class="hljs-literal">true</span>
</code></pre>
<h3 id="heading-default-request-generation-options">Default Request Generation Options</h3>
<p>Overriding the configuration on how the requests are being generated can be done by updating the following attributes under <code>gql-generator.output</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">gql-generator:</span>
  <span class="hljs-attr">output:</span>
    <span class="hljs-attr">directory:</span> <span class="hljs-string">./output</span>
    <span class="hljs-attr">useVariables:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">maxDepth:</span> <span class="hljs-number">10</span>
    <span class="hljs-attr">rawRequests:</span> <span class="hljs-literal">false</span>
</code></pre>
<ul>
<li><p><code>output.directory</code> - path to the directory where the generated files should be stored at</p>
</li>
<li><p><code>output.useVariables</code> - flag to decide if the generated requests should have inline input or dedicated variable files</p>
</li>
<li><p><code>output.maxDepth</code> - maximum allowed depth for the generated requests, used to avoid infinite recursions in the requests</p>
</li>
<li><p><code>output.rawRequests</code> - flag to indicate if the raw requests (<code>.graphql</code> files) should also be stored. This flag has to be set to <code>true</code> if <code>output.postman</code> is set to <code>false</code></p>
</li>
</ul>
<h3 id="heading-default-postman-collection-configuration">Default Postman Collection Configuration</h3>
<p>The plugin will, by default, generate a Postman Collection that will be called based on <code>${service}-${stage}</code> and fetch the URL and API Key from the deployed API.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">gql-generator:</span>
  <span class="hljs-attr">output:</span>
    <span class="hljs-attr">postman:</span>
      <span class="hljs-attr">collectionName:</span> <span class="hljs-string">test-name</span>
      <span class="hljs-attr">url:</span> <span class="hljs-string">https://test.com/graphql</span>
      <span class="hljs-attr">apiKey:</span> <span class="hljs-string">abc-123</span>

<span class="hljs-comment"># OR</span>

<span class="hljs-attr">gql-generator:</span>
  <span class="hljs-attr">output:</span>
    <span class="hljs-attr">postman:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>Developers can override this configuration by updating the following attributes:</p>
<ul>
<li><p><code>output.postman</code> - can be set to <code>false</code> to avoid the Postman Collection being generated, for that scenario <code>output.rawRequests</code> will be required to be <code>true</code></p>
</li>
<li><p><code>output.postman.collectionName</code> - used to override the name to be used for the generated collection</p>
</li>
<li><p><code>output.postman.url</code> - used to override the API endpoint, especially useful if the API has a custom domain configured or you want to use the path to the CDN or Proxy</p>
</li>
<li><p><code>output.postman.apiKey</code> - used to override the API KEY to be used to authenticate the requests</p>
</li>
</ul>
<h3 id="heading-uploading-files-to-s3">Uploading files to S3</h3>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Developers or CI/CD pipelines will require credentials with write access to the desired bucket.</div>
</div>

<p>The output is only saved locally by default on the machine that generates the files, but there is the option to upload the files to S3 at the same time, making it even easier to share the results with other developers or teams.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">gql-generator:</span>
  <span class="hljs-attr">output:</span>
    <span class="hljs-attr">s3:</span>
      <span class="hljs-attr">bucketName:</span> <span class="hljs-string">gql-output-bucket</span>
      <span class="hljs-attr">folderPath:</span> <span class="hljs-string">s3folder/path</span>
      <span class="hljs-attr">skipLocalSaving:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>In order to enable the export to S3 features, developers will need to update the following configuration under <code>gql-generator.output.s3</code>:</p>
<ul>
<li><p><code>output.s3.bucketName</code> - <strong>mandatory</strong> field, has to contain the name of an <strong>existing bucket</strong></p>
</li>
<li><p><code>output.s3.folderPath</code> - used to override the folder path in S3 where the files will be stored, by default it will create the following folder structure to store the files <code>${service}/${stage}</code></p>
</li>
<li><p><code>output.s3.skipLocalSaving</code> - flag to indicate if the files should be stored locally or only uploaded to S3</p>
</li>
</ul>
<h2 id="heading-cli-commands">CLI commands</h2>
<p>Developers might want to trigger the request generation without wanting to redeploy the API again, the plugin exposes some CLI commands to allow for that scenario.</p>
<h3 id="heading-schema-validation">Schema Validation</h3>
<p>When creating or updating a Schema developers might want to validate that it's formatted appropriately.</p>
<p>The plugin exposes the following command to allow developers to validate the provided schema:</p>
<pre><code class="lang-bash">serverless gql-generator validate-schema
</code></pre>
<p>Once the above line has been executed on the terminal, it will prompt any possible issues or confirm that the schema is valid and ready to be used.</p>
<h3 id="heading-requests-generation">Requests generation</h3>
<p>By default, the plugin generates all requests on every deployment, but there could be a scenario where one would like to generate them without deploying the API again.</p>
<p>Developers might use the following command to trigger the Request generation:</p>
<pre><code class="lang-bash">serverless gql-generator generate
</code></pre>
<p>Once executed, the plugin will generate all requests based on the configuration.</p>
<p>This command can be especially useful to confirm that the plugin is configured as expected or to generate the requests again in case the output folder has been added to <code>.gitignore</code>.</p>
<h2 id="heading-cicd-integration">CI/CD integration</h2>
<p>This plugin is easily integrated into one's CI/CD pipeline as it will be triggered automatically during the deployment process.</p>
<p>To access the generated files, developers might choose between:</p>
<ul>
<li><p><strong>Workflow Artifacts</strong> - Configuring the pipeline to <a target="_blank" href="https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts">export the artifacts</a> generated during the process would be equivalent to generating the files locally, but the developers would need to download them from the workflow execution.<br />  This approach is preferred for example for feature branches, where the output might change multiple times during the development.</p>
</li>
<li><p><strong>S3 Export</strong> - Output files will be uploaded automatically to S3 for easier access and sharing.<br />  This is the preferred approach for stable or shared branches, such as <code>dev</code> or <code>main</code>.</p>
</li>
</ul>
<h1 id="heading-conclusions">Conclusions</h1>
<p>Adding the serverless-gql-generator plugin to your stack can help you and your team save time by automating the process of generating and sharing GraphQL requests.</p>
<p>I would love to hear about your experience using the plugin. Please use <a target="_blank" href="https://github.com/Lorenzohidalgo/serverless-gql-generator/issues">GitHub Issues</a> to report any issues while using the plugin or requesting new features.</p>
]]></content:encoded></item><item><title><![CDATA[Unlocking the Power of DynamoDB Condition Expressions]]></title><description><![CDATA[TL;DR;This article explores DynamoDB condition expressions, highlighting their potential to enhance database operations. They help prevent accidental data overwriting and facilitate conditional updates. The article also details how to handle errors e...]]></description><link>https://lhidalgo.dev/unlocking-the-power-of-dynamodb-condition-expressions</link><guid isPermaLink="true">https://lhidalgo.dev/unlocking-the-power-of-dynamodb-condition-expressions</guid><category><![CDATA[AWS]]></category><category><![CDATA[DynamoDB]]></category><category><![CDATA[serverless]]></category><category><![CDATA[js]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Tue, 30 Jan 2024 09:41:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/fPkvU7RDmCo/upload/e185fdf53e51deef61a95dcff0951079.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR;</strong><br />This article explores DynamoDB condition expressions, highlighting their potential to enhance database operations. They help prevent accidental data overwriting and facilitate conditional updates. The article also details how to handle errors efficiently when condition checks fail, thus reducing the need for additional requests. By leveraging these expressions, developers can create more efficient and robust applications.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>Are you tired of your <code>PutItem</code> operations overwriting existing items?<br />Or your <code>UpdateItem</code> inserting a new object if the provided keys don't exist?<br />Are you still reading an object before updating it to ensure its current state?</p>
<p>If you replied to any of the above questions with a yes, the title triggered your inner refactoring freak or you're just keen to learn new approaches, then this article is for you. In this article we will discover and learn how to take the most advantage of (what I think) is a somewhat unknown feature: DynamoDB Condition Expressions.</p>
<h1 id="heading-dynamodb-expressions">DynamoDB Expressions</h1>
<p>When working with DynamoDB developers use <code>expressions</code> to state what actions or conditions they want DynamoDB to process.</p>
<p>There are different types of expressions that developers can use:</p>
<ul>
<li><p><code>Projection Expressions</code> - Similar to the <code>SELECT</code> statement in SQL, this type of expression is used to select what attributes you want DynamoDB to return when reading objects. By default, DynamoDB returns all attributes (same as using <code>SELECT *</code>).</p>
</li>
<li><p><code>Update Expressions</code> - Similar to the <code>UPDATE</code> statement in SQL, this expression is used in the <code>UpdateItem</code> requests to specify what changes you want to apply to the desired object. Developers can <code>ADD</code>, <code>SET</code>, <code>REMOVE</code> or <code>DELETE</code> attributes.</p>
</li>
<li><p><code>Condition Expressions</code> - This article's <em>star of the show;</em> could be compared to the <code>WHERE</code> clause of a SQL statement. This expression type will allow developers, as the name implies, to ensure a condition is met before the data is manipulated.</p>
</li>
</ul>
<h1 id="heading-using-condition-expressions">Using Condition Expressions</h1>
<p>Developers can use <code>condition expressions</code> in any DynamoDB operation that manipulates data, such as <code>PutItem</code> or <code>UpdateItem</code>, and using such expressions opens the door to a whole new set of business logic and fail-safes that can be implemented without adding any additional requests to DynamoDB.</p>
<h2 id="heading-avoid-overriding-existing-objects">Avoid overriding existing objects</h2>
<p>DynamoDB will, by default, override any existing data if an <code>PutItem</code> operation is triggered with existing keys.</p>
<p>There are scenarios where one would like to store a new object but only if it doesn't exist, for example, if we want to do some kind of idempotency check or if we want to avoid overriding existing data.</p>
<p>To do that, one could approach the issue by doing a <code>Get</code>/<code>Query</code> and only triggering the <code>Put</code> if the item doesn't exist, but using condition expressions allows us to do everything in a single request:</p>
<pre><code class="lang-javascript">  <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> PutCommand({
    ...,
    <span class="hljs-attr">ConditionExpression</span>: <span class="hljs-string">"attribute_not_exists(#id)"</span>,
    <span class="hljs-attr">ExpressionAttributeNames</span>: {
      <span class="hljs-string">"#id"</span>: <span class="hljs-string">"PK"</span>,
    },
  });
  <span class="hljs-keyword">await</span> client.send(command);
</code></pre>
<p>In the above example snippet we're doing a <code>PutItem</code> request and adding the condition expression of <code>attribute_not_exists(#id)</code>, this will indicate DynamoDB to only store the provided object if the current primary key (<code>PK</code>) is not in use.</p>
<p>By doing it, DynamoDB won't override any existing data and will also throw an error if the condition is not met (an object already exists).</p>
<p>Full example <a target="_blank" href="https://github.com/Lorenzohidalgo/dynamodb-conditional-samples/blob/main/samples/putItemWithCondition.js">here</a>.</p>
<h2 id="heading-update-only-existing-items">Update only existing items</h2>
<p>Similar to the insert, DynamoDBs behaviour on <code>UpdateItem</code> can also be a tad counter-intuitive, as it will execute and <code>UPSERT</code> operations and not an <code>UPDATE</code>.</p>
<p>For some scenarios it won't be an issue to always execute <code>UPSERT</code> operations by default, but in general one will want the <code>UPDATE</code> operation to fail if the record doesn't exist to avoid creating new records with only partial data that will probably trigger errors downstream.</p>
<p>Implementing that change is very easy, as one just needs to add a simple condition expression as follows.</p>
<pre><code class="lang-javascript">    <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> UpdateCommand({
      ...,
      <span class="hljs-attr">ConditionExpression</span>: <span class="hljs-string">"attribute_exists(#id)"</span>,
      <span class="hljs-attr">ExpressionAttributeNames</span>: {
        <span class="hljs-string">"#id"</span>: <span class="hljs-string">"PK"</span>,
      },
    });
    <span class="hljs-keyword">await</span> docClient.send(command);
</code></pre>
<p>In the above example snippet we're doing a <code>UpdateItem</code> request and adding the condition expression of <code>attribute_exists(#id)</code>, this will indicate DynamoDB to only update the desired object if the current primary key (<code>PK</code>) exists.</p>
<p>With this condition, if DynamoDB can't find an update with the provided keys, the current primary key (<code>PK</code>) will not exist and the condition would fail, meaning that the <code>UpdateItem</code> request will throw an error.</p>
<p>Full example <a target="_blank" href="https://github.com/Lorenzohidalgo/dynamodb-conditional-samples/blob/main/samples/putItemWithCondition.js">here</a>.</p>
<h2 id="heading-conditional-updates">Conditional Updates</h2>
<p>But the magic of Condition Expressions doesn't stop on avoiding unexpected behaviours from DynamoDB, they open a whole new range of possibilities.</p>
<p>For example, let's imagine a scenario where, during the login flow, you want to check if the account is frozen or not (user should not be able to log in) and if the account is active, update a field storing the timestamp and device information of the current login.</p>
<p>For this scenario, one could first query the data to check the account status and if the check is successfull send another request to update the object with the desired login information. This approach is technically correct and would probably work for most requirements, but it opens the following concerns:</p>
<ul>
<li><p><strong>Number of Requests</strong> - The most common scenario will be that the account is not frozen and the login should succeed, meaning that for almost all logins two requests will be sent to DynamoDB.</p>
</li>
<li><p><strong>Data Consistency</strong> - This approach would be unable to handle or control any change on the data between both requests, meaning that, if the account is being deactivated at the same time, the login could succeed when it should have failed.</p>
</li>
</ul>
<p>With the use of the Condition Expression we can move that business logic to be execute on DynamoDBs side.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🤯</div>
<div data-node-type="callout-text">Did you know that you can edit nested attributes directly? You just need to use the <code>.</code> (dot) or <code>[x]</code> notation when referencing it! <a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Attributes.html#Expressions.Attributes.NestedAttributes">Check the docs</a></div>
</div>

<pre><code class="lang-javascript">    <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> UpdateCommand({
      ...,
      <span class="hljs-attr">ConditionExpression</span>: <span class="hljs-string">"attribute_exists(#id) AND #obj.#key = :expected"</span>,
      <span class="hljs-attr">ExpressionAttributeNames</span>: {
        <span class="hljs-string">"#id"</span>: <span class="hljs-string">"PK"</span>,
        <span class="hljs-string">"#obj"</span>: <span class="hljs-string">"accountInformation"</span>,
        <span class="hljs-string">"#key"</span>: <span class="hljs-string">"isFrozen"</span>,
      },
      <span class="hljs-attr">ExpressionAttributeValues</span>: {
        <span class="hljs-string">":expected"</span>: <span class="hljs-literal">false</span>,
      },
    });
    <span class="hljs-keyword">await</span> docClient.send(command);
</code></pre>
<p>In this example snippet we're adding the condition expression of <code>attribute_exists(#id) AND #obj.#key = :expected</code>, this will indicate DynamoDB to only update the desired object if the current primary key (<code>PK</code>) exists and if the nested attribute <code>accountInformation.isFrozen</code> is set to <code>false</code>.</p>
<p>With this condition, we move the business logic to check the account status to be executed by DynamoDB directly, meaning that the object will only be updated if the condition is met. This way we only need to consider that the login should only fail if the <code>UpdateItem</code> request has failed.</p>
<p>Full example <a target="_blank" href="https://github.com/Lorenzohidalgo/dynamodb-conditional-samples/blob/main/samples/putItemWithCondition.js">here</a>.</p>
<h2 id="heading-handling-conditional-check-failures">Handling Conditional Check Failures</h2>
<p>Now we know how to implement the condition expressions but we are missing what to do when the condition fails.</p>
<p>What if the expression contains multiple conditions and we want to have a custom error message depending on wich condition failed, should we trigger a new request to query the object?</p>
<p>The answer is no, and this is probably one of the hidden features that will help you the most. At this point, you are probably aware of the <code>ReturnValues</code> option to configure what information you would like DynamoDB to return, but did you know there is also <code>ReturnValuesOnConditionCheckFailure</code>?</p>
<p>It works similarly as the <code>ReturnValues</code> option, but with only two available options:</p>
<ul>
<li><p><code>NONE</code> - the default, the thrown error won't have any information regarding the current object attributes and values.</p>
</li>
<li><p><code>ALL_OLD</code> - setting this option will request DynamoDB to throw an error that contains the old (current) object information in the response.</p>
</li>
</ul>
<pre><code class="lang-javascript">  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> UpdateCommand({
      ...,
      <span class="hljs-attr">ReturnValuesOnConditionCheckFailure</span>: <span class="hljs-string">"ALL_OLD"</span>,
    });

    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> docClient.send(command);
    <span class="hljs-keyword">return</span> response;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">if</span> (!(error <span class="hljs-keyword">instanceof</span> ConditionalCheckFailedException)) <span class="hljs-keyword">throw</span> error;
    <span class="hljs-keyword">const</span> oldRecord = unmarshall(error.Item);
    <span class="hljs-comment">// Apply any logic to check why the update failed</span>
  }
</code></pre>
<p>The above snippet showcases how one could approach the error handling for conditional expressions with three simple steps:</p>
<ol>
<li><p>Set <code>ReturnValuesOnConditionCheckFailure: "ALL_OLD"</code> to ensure you receive the object information if the condition fails.</p>
</li>
<li><p>Ensure you only catch the expected exception types by re-throwing any error that is not an instance of <code>ConditionalCheckFailedException</code>.</p>
</li>
<li><p>Retrieve and unmarshall the record data from the returned error with <code>unmarshall(error.Item)</code>. The <code>Item</code> value will be the marshalled object that we tried to update, it's important to unmarshall it or ensure we access it's properties respecting the marshalling.</p>
</li>
</ol>
<p>After those steps have been implemented, one can add any additional desired business logic to check why the operation failed and handle the error appropriately based on which of the conditions failed.</p>
<p>Full example <a target="_blank" href="https://github.com/Lorenzohidalgo/dynamodb-conditional-samples/blob/main/samples/putItemWithCondition.js">here</a>.</p>
<h1 id="heading-conclusions">Conclusions</h1>
<p>In conclusion, DynamoDB condition expressions provide a powerful tool that can greatly enhance the efficiency and reliability of your database operations. They allow developers to implement complex business logic directly within their database operations, reducing the number of requests and mitigating risks of data inconsistency.</p>
<p>This feature can help prevent unintended data overwriting, ensure updates only apply to existing items, and enable conditional updates based on specific criteria. With the ability to return current object data when condition checks fail, developers can handle errors more effectively without making additional requests.</p>
<p>By fully utilizing this feature, you can unlock the true potential of DynamoDB and create more robust and efficient applications.</p>
]]></content:encoded></item><item><title><![CDATA[[Webinar] Building Our Own AI Chatbot: Making Santa]]></title><description><![CDATA[https://www.youtube.com/watch?v=300YgXGADH8]]></description><link>https://lhidalgo.dev/webinar-building-our-own-ai-chatbot-making-santa</link><guid isPermaLink="true">https://lhidalgo.dev/webinar-building-our-own-ai-chatbot-making-santa</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AI]]></category><category><![CDATA[chatbot]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Fri, 15 Dec 2023 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710518624225/2f6837f4-ca98-4f0c-ac3e-21f548d8d259.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=300YgXGADH8">https://www.youtube.com/watch?v=300YgXGADH8</a></div>
]]></content:encoded></item><item><title><![CDATA[Building Our Own AI ChatBot: Making Santa]]></title><description><![CDATA[Introduction
Serverless Guru has organized a Serverless Holiday Hackathon where participants compete during the first half of December 2023 to see who can develop the best holiday-themed chat application that uses any LLM.
As part of the Hackathon or...]]></description><link>https://lhidalgo.dev/building-our-own-ai-chatbot-making-santa</link><guid isPermaLink="true">https://lhidalgo.dev/building-our-own-ai-chatbot-making-santa</guid><category><![CDATA[AWS]]></category><category><![CDATA[Amazon Bedrock]]></category><category><![CDATA[websockets]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Tue, 12 Dec 2023 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710430641026/93ca995e-4adc-44ce-bb7b-f77083b2e79c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction"><strong>Introduction</strong></h2>
<p>Serverless Guru has organized a <a target="_blank" href="https://hackathon.serverless.guru/">Serverless Holiday Hackathon</a> where participants compete during the first half of December 2023 to see who can develop the best holiday-themed chat application that uses any LLM.</p>
<p>As part of the Hackathon organization, we developed a demo app to showcase to participants how one could build a relatively simple chat application where the LLM acts like Santa.</p>
<p>This article is meant to aid any participant who might be currently stuck with their project while also acting as an example of the article they need to write as part of the project submission.</p>
<p>Our Demo will be live until the end of December 2023 and can be found under the following link: <a target="_blank" href="https://santa.serverlessguru.com/">https://santa.serverlessguru.com/</a></p>
<h2 id="heading-demo-video"><strong>Demo Video</strong></h2>
<p>Some Loom videos have been recorded to showcase the demo application and to provide more context before we do a deep dive into the actual Back-End architecture and development process.</p>
<h3 id="heading-fe-example"><strong>FE Example</strong></h3>
<p>The following video provides a quick introduction to the application, its capabilities, and how one could use it from the Front-End app.</p>
<p><div class="embedded-content"><a class="embed-card" data-card-width="600px" data-card-key="2e4d628b39a64b99917c73956a16b477" data-card-controls="0" data-card-theme="light" href="https://www.loom.com/share/f950e9dd55b04889a6e77705c8278592?sid=6d5af159-7866-4434-91bf-31d308062fa"></a></div>]</p>
<h3 id="heading-api-example-postman"><strong>API Example (Postman)</strong></h3>
<p>Another video has been recorded of the actual WebSocket API behaviour to provide more context into the actual workings of the developed API.</p>
<p><div class="embedded-content"><a class="embed-card" data-card-width="600px" data-card-key="2e4d628b39a64b99917c73956a16b477" data-card-controls="0" data-card-theme="light" href="https://www.loom.com/share/9882adc0e4c643c8b468766376e2f71d?sid=7b769a32-697c-4a1c-912d-2c423cf7f15"></a></div>]</p>
<h2 id="heading-challenges-faced-during-development"><strong>Challenges Faced During Development</strong></h2>
<p>As part of this section, and before showcasing the full solution, we want to introduce some of the challenges we faced during the development and how we resolved them.</p>
<h3 id="heading-connecting-to-an-llm"><strong>Connecting to an LLM</strong></h3>
<p>The main requirement of the Hackathon is that the app has to be holiday-themed and take advantage of the use of an LLM.</p>
<h3 id="heading-conversation-chain-and-memory"><strong>Conversation Chain and Memory</strong></h3>
<p>We introduced the usage of the Langchain JS framework to simplify the implementation of a Conversation Chain and the storage of the Conversation History.</p>
<p>Enabling this allows the model to have access to the context of the full conversation and generate more context-aware and valuable responses.</p>
<h3 id="heading-getting-streamed-responses"><strong>Getting Streamed Responses</strong></h3>
<p>As the cherry on top, we also wanted to implement streamed responses to the user.</p>
<p>This would allow us to generate a more interactive experience on the Front End by completing the response dynamically.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65789a8eda280d820e60976a_Streamed-Response.webp" alt="Getting a streamed response to the user" /></p>
<p>To be able to receive streamed responses and use a conversation chain, we needed to implement a callback on Langchain.</p>
<p>Here is a simplified implementation example:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> chain.call({ <span class="hljs-attr">input</span>: userInput }, [
    {
      <span class="hljs-keyword">async</span> handleLLMNewToken(token) {
        streamedResponse += token;
        <span class="hljs-built_in">console</span>.log(streamedResponse);
      },
    },
  ]);
</code></pre>
<p>The full code snippet can be found in the following <a target="_blank" href="https://github.com/serverless-guru/bedrock-samples/blob/main/samples/langchain/langchain_bedrock_memory_streamed.js">repository</a>.</p>
<h2 id="heading-avoiding-timeouts-websocket-api"><strong>Avoiding Timeouts - WebSocket API</strong></h2>
<p>As mentioned during our Webinar <a target="_blank" href="https://www.youtube.com/watch?v=o2f1oCbqSpI">Building Your First Serverless API with Langchain</a> one of the challenges developers will face is the execution time taken by the LLMs to process some prompts.</p>
<p>During the Webinar, we presented different options, such as AppSync Subscriptions and WebSocket APIs.</p>
<p>In this case, we decided to develop a WebSocket API, which resolved the need for bi-directional communication but introduced some additional challenges.</p>
<h3 id="heading-decoupling-long-lasting-execution"><strong>Decoupling long-lasting execution</strong></h3>
<p>Similar to any other API, a WebSocket API still has a 30-second timeout for the Back End Resources to confirm a message was received correctly.</p>
<p>The main idea is to decouple the logic into two different flows.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65789da6e0f5b67d2426ebfd_Incoming-Message-Flow.webp" alt="A flow limited by the 30 - second timeout responsible for handling incoming messages." /></p>
<p>The first flow, as shown in the image above, is the one limited by the 30-second timeout and is responsible for handling incoming messages. The Message Handler Lambda would process any validation logic required, trigger an event, and reply to the user stating that the message was received gracefully.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65789db52918474582f1cfb0_Decoupled-Message_Flow.webp" alt="Decoupled flow that can take as long as needed and is only limited by maximum execution time of the lambda and 10 min idle timeout." /></p>
<p>The second flow is completely decoupled and can take as long as needed being only limited by the maximum execution time of the lambda and the 10-minute idle timeout of WebSocket APIs. In this Flow, the Message Processor Lambda is triggered by the events generated by the previous flow, executes any processing logic, and takes advantage of the bi-directional communication capabilities to send as many messages as needed back to the user.</p>
<h3 id="heading-authentication-and-state-management"><strong>Authentication and State Management</strong></h3>
<p>One of the main issues in choosing a WebSocket API is the need to implement the correct state management logic to ensure that the messages are being processed in the required order and that no one tries to exploit the API.</p>
<p>To handle the correct usage and authentication we implemented the following state machine:</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65789dc488b2f9d743b29b9c_State-Machine.webp" alt="State machine diagram used to handle correct usage and authentication." class="image--center mx-auto" /></p>
<p>The most relevant parts of the implemented state machine are:</p>
<ul>
<li><p>No messages will be processed until the user sends a valid authentication message.</p>
</li>
<li><p>If authentication fails, the state will be updated as <strong>'Authentication Failed'</strong>, and no further action/message will be processed.</p>
</li>
<li><p>User prompts will be processed one at a time and only if the state is in <strong>'Connection Ready'</strong> when the prompt is received.</p>
</li>
</ul>
<h2 id="heading-architecture-overview"><strong>Architecture Overview</strong></h2>
<p>After seeing the different challenges that we faced and how we resolved them, we are ready to jump straight into the final architecture.</p>
<p>As part of our final application, we developed two different APIs:</p>
<h3 id="heading-management-api"><strong>Management API</strong></h3>
<p>On one side we have the Management API, which is intended to handle part of the authorization logic.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65789dd6679869857fc99a29_Management-API.webp" alt="Diagram showing flow to and from API Gateway specifically Management API" /></p>
<p>As part of this REST API, we have two different endpoints:</p>
<ul>
<li><p><strong>'/generateApiKey'</strong>: This endpoint, intended to only be used internally, handles the API Key generation. It can be seen as an API Key vending machine. As WebSocket APIs don’t support API Keys, we’ll store the identifier and associated usage limits on a DynamoDB table.</p>
</li>
<li><p><strong>'/retrieveConfig/{configId}'</strong>: This endpoint, used by the Front End application, is used to retrieve the API Key information necessary to send the authentication message as part of the WebSocket API.</p>
</li>
</ul>
<h3 id="heading-websocket-api"><strong>WebSocket API</strong></h3>
<p>The “main course” would be the WebSocket API, the main API used to implement the Santa Chat application logic.</p>
<p>This API could be divided into the following sections:</p>
<h3 id="heading-default-actions"><strong>Default Actions</strong></h3>
<p>Default actions, or routes, would be the ones handling the default/required functionalities.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65789decc96bb107f5e0e882_Websocket-API-Default-Action.webp" alt="Diagram showing default actions and routes to and from Websocket API" /></p>
<p>As part of this section, we have the following routes:</p>
<ul>
<li><p><strong>'$connect'</strong>: This route is triggered for every connection request. For this route, we implemented some custom logic to create a new <strong>'session'</strong> record for the received <strong>'connectionId'</strong>. This newly created record is used to manage the different states and ensure we’re able to limit the usage as required.</p>
</li>
<li><p><strong>'$disconnect'</strong>: This route is triggered whenever a connection is closed. Here we implemented some clean-up logic to delete any stored information linked to the current connection.</p>
</li>
<li><p><strong>'$default'</strong>: This is the fall-back route, which is triggered when a route for the given action cannot be found. For our specific use case, triggering this route will always return an error message.</p>
</li>
</ul>
<h3 id="heading-custom-actions"><strong>Custom Actions</strong></h3>
<p>As part of the default and required actions, we also implemented some custom routes/actions.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65789df8d6dee462fba1772a_Websocket-API-Custom-Action.webp" alt="Diagram showing Custom actions to and from Websocket API" /></p>
<p>These new custom routes will handle the following logic:</p>
<ul>
<li><p><strong>'authenticate'</strong>: This route will check that the provided API Key is valid and update the Session state accordingly. If the authentication is successful, a <strong>'Authentication Completed'</strong> event will be added to the custom EventBridge bus. If it fails, no event will be triggered and the connection won’t accept any further messages.</p>
</li>
<li><p><strong>'send_message'</strong>: This route is the one responsible for handling any received user prompts. This Lambda will also ensure that the Session State is in the correct status and, if it is, it will add a <strong>'Prompt Received'</strong> event to the custom EventBridge bus and set the new Session State.</p>
</li>
</ul>
<h3 id="heading-decoupled-events"><strong>Decoupled Events</strong></h3>
<p>The magic sauce, decoupling the events, could be the most relevant part of the architecture.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65789e0600b513fc6bb9e9e7_Decoupled-Events.webp" alt="Diagram showing Decoupled events utilizing AWS bedrock to ensure API never times out." /></p>
<p>By decoupling the processing/requests sent to AWS Bedrock to an asynchronous process we ensure that our API should never timeout.</p>
<p>We defined and implemented the following events:</p>
<ul>
<li><p><strong>'Authentication Completed'</strong>: This event is triggered automatically when the authentication process is completed. The main objective of this event is to trigger an initial prompt to Bedrock, that will start the conversation by introducing itself as Santa and setting up the appropriate context.</p>
</li>
<li><p><strong>'Prompt Received'</strong>: This event is triggered every time we receive a prompt. The SendMessage Handler Lambda will be responsible for formatting and sending the prompt to Bedrock and sending the streamed response to the user.</p>
</li>
</ul>
<h2 id="heading-next-steps"><strong>Next Steps</strong></h2>
<p>As mentioned in the article, the approach taken was to develop an MVP with minimal features to showcase how a simple chat application could work.</p>
<p>There is an almost infinite amount of possible next steps and improvements that could be done, here are a few that we could think of:</p>
<ul>
<li><p>Enable voice-to-text and text-to-voice transcription to take the application a step further and make it feel more like a real conversation and not just a chat.</p>
</li>
<li><p>Improve Authentication and allow for persistent message history.</p>
</li>
<li><p>Add any additional feature, such as implementing an Agent to allow the LLM to query another API to f.e.: List possible gift ideas or build a custom wishlist to send to Santa.</p>
</li>
</ul>
<h2 id="heading-conclusions"><strong>Conclusions</strong></h2>
<p>In conclusion, the process of building an AI chatbot, particularly one themed around Santa for our holiday hackathon, presented a number of challenges but also provided a rich learning experience.</p>
<p>From ensuring efficient handling of long-lasting executions to managing the state of WebSocket APIs, we navigated various complexities and came up with innovative solutions.</p>
<p>This project not only showcases the potential of Large Language Models (LLMs) in crafting engaging conversational experiences but also demonstrates the importance of a well-thought-out architecture in managing the unique challenges of real-time, interactive applications.</p>
<p>As we continue to explore the world of serverless applications and AI chatbots, we look forward to sharing more insights and learnings with our community.</p>
]]></content:encoded></item><item><title><![CDATA[[Webinar] Building Your First Serverless API with Langchain]]></title><description><![CDATA[https://www.youtube.com/watch?v=o2f1oCbqSpI]]></description><link>https://lhidalgo.dev/webinar-building-your-first-serverless-api-with-langchain</link><guid isPermaLink="true">https://lhidalgo.dev/webinar-building-your-first-serverless-api-with-langchain</guid><category><![CDATA[AWS]]></category><category><![CDATA[langchain]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Fri, 08 Dec 2023 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710518362299/4398261c-3dcc-48b8-a104-980f109a042c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=o2f1oCbqSpI">https://www.youtube.com/watch?v=o2f1oCbqSpI</a></div>
]]></content:encoded></item><item><title><![CDATA[AWS AppSync Subscriptions: Detaching Prolonged Operations]]></title><description><![CDATA[TL;DR: This article discusses how to use AppSync Subscriptions to decouple long-running tasks from front-end requests in a serverless chat application. It provides a step-by-step guide to implement this solution, including an architecture overview, d...]]></description><link>https://lhidalgo.dev/aws-appsync-subscriptions-detaching-prolonged-operations</link><guid isPermaLink="true">https://lhidalgo.dev/aws-appsync-subscriptions-detaching-prolonged-operations</guid><category><![CDATA[AppSync]]></category><category><![CDATA[AWS]]></category><category><![CDATA[aws lambda]]></category><category><![CDATA[serverless]]></category><category><![CDATA[serverless framework]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Tue, 05 Dec 2023 08:47:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/npxXWgQ33ZQ/upload/22ab153a457c3091114d23f0fcc3138c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>TL;DR: This article discusses how to use AppSync Subscriptions to decouple long-running tasks from front-end requests in a serverless chat application. It provides a step-by-step guide to implement this solution, including an architecture overview, decoupling and processing steps, prerequisites, GraphQL schema, AppSync API configuration, DataSources and Resolvers, and Lambda function setup. The sample code and complete implementation can be found in the provided GitHub repository.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>When building APIs, developers often face the issue of long-running tasks timing out requests from the Front End or difficulty decoupling those longer tasks from the actual FE requests while informing it of the execution status.</p>
<p>In this article, and as part of the <a target="_blank" href="https://hackathon.serverless.guru/">Serverless Holiday Hackathon</a>, we will review how developers can take advantage of AppSync Subscriptions to decuple long-running tasks from the actual FE request.</p>
<h1 id="heading-architecture-overview">Architecture Overview</h1>
<p>The Hackathon challenges participants to build holiday-themed chat applications that use Generative AI.</p>
<p>While building such an application, developers will probably face the possible difficulties:</p>
<ul>
<li><p>Requests to Badrock or any other LLM API could entice long-running requests to generate longer responses, these could take longer than 30 seconds and timeout the requests</p>
</li>
<li><p>Not knowing how to take advantage of streamed responses, which would provide the final user with a more interactive experience.</p>
</li>
</ul>
<p>With this example, we will cover how to resolve both scenarios in two simple steps, while still leaving room for improvement and personalization.</p>
<h2 id="heading-step-1-decoupling">Step 1: Decoupling</h2>
<p>As the first step, we will need to decouple the front-end request from the actual processing. To do so, we can configure a JS Resolver to send the message to be processed to a SQS queue.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701719096831/de956cb7-d165-4655-91ef-10d5c94728f5.png" alt="sendPrompt Mutation overview" class="image--center mx-auto" /></p>
<p>The flow would look like this:</p>
<ol>
<li><p>The user sends a request to AppSync.</p>
</li>
<li><p>The JS Resolver creates a unique identifier for the received prompt and adds the message to the SQS Queue.</p>
</li>
<li><p>AppSync returns the unique identifiers to the User.</p>
</li>
</ol>
<p>The User will need the response for the following step.</p>
<h2 id="heading-step-2-process-and-notify">Step 2: Process and Notify</h2>
<p>The second step will handle the prompt processing and notifying the user by sending a dummy mutation request that will trigger a subscription.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701719513475/c2259d07-aa7c-48d7-8a6f-1b4e727615a5.png" alt class="image--center mx-auto" /></p>
<p>The flow for this step would be composed of the following steps:</p>
<ol>
<li><p>The user subscribes via AppSync to updates on the <code>streamedResponse</code> mutation using the provided identifiers in the previous response.</p>
</li>
<li><p>The SQS Queue will trigger the Lambda for each message added to the same Queue by the above-explained mutation.</p>
</li>
<li><p>The Lambda Function will send a <code>streamedResponse</code> mutation request for all updates that we want to notify the user with.</p>
</li>
<li><p>Each request sent as a <code>streamedResponse</code> mutation will trigger subscribed users to be notified by every response that matches the filtering requirements.</p>
</li>
</ol>
<h1 id="heading-implementing-the-solution">Implementing the solution</h1>
<p>In this section, we will go over how to implement the solution that we described in the previous step.</p>
<p>All details and code can be found in the following <a target="_blank" href="https://github.com/Lorenzohidalgo/appsync-decoupling-sample">sample application repository</a>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To correctly follow and deploy the sample application, developers will need to fulfill the following requisites:</p>
<ul>
<li><p>Node JS installation</p>
</li>
<li><p>AWS Account to deploy the API</p>
</li>
<li><p>Postman or GraphBolt to send requests and test the flow</p>
</li>
</ul>
<h2 id="heading-graphql-schema">GraphQL Schema</h2>
<p>A mock schema has been defined for this application, part of the schema can be seen here:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">schema</span> {
  <span class="hljs-symbol">query:</span> Query
  <span class="hljs-symbol">mutation:</span> Mutation
  <span class="hljs-symbol">subscription:</span> Subscription
}

<span class="hljs-keyword">type</span> Query <span class="hljs-meta">@aws_api_key</span> <span class="hljs-meta">@aws_iam</span> {
  <span class="hljs-symbol">getSessionId:</span> ID!
}

<span class="hljs-keyword">type</span> Mutation {
  sendPrompt(<span class="hljs-symbol">userPrompt:</span> userPrompt!): promptResponse <span class="hljs-meta">@aws_api_key</span> <span class="hljs-meta">@aws_iam</span>
  streamedResponse(<span class="hljs-symbol">streamedResponseInput:</span> StreamedResponseInput!): StreamedResponse <span class="hljs-meta">@aws_iam</span>
}

<span class="hljs-keyword">type</span> Subscription <span class="hljs-meta">@aws_api_key</span> <span class="hljs-meta">@aws_iam</span> {
  onStreamedResponse(<span class="hljs-symbol">sessionId:</span> ID!): StreamedResponse <span class="hljs-meta">@aws_subscribe</span>(<span class="hljs-symbol">mutations:</span> [<span class="hljs-string">"streamedResponse"</span>])
}
</code></pre>
<p>The most important part of it is the auth directives for the mutations, where <code>streamedResponse</code> is only enabled for <code>@aws_iam</code>.</p>
<p>This is an important configuration aspect as we want only our Back End services to be able to trigger this mutation.</p>
<h2 id="heading-appsync-api">AppSync API</h2>
<p>To configure the AppSync API using Serverless Framework we will be taking advantage of the <a target="_blank" href="https://github.com/sid88in/serverless-appsync-plugin">Serverless AppSync Plugin</a>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">appSync:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">${self:custom.base}-appsync</span>
  <span class="hljs-attr">logging:</span>
    <span class="hljs-attr">level:</span> <span class="hljs-string">ALL</span>
    <span class="hljs-attr">retentionInDays:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">xrayEnabled:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">authentication:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">AWS_IAM</span>
  <span class="hljs-attr">additionalAuthentications:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">API_KEY</span>
  <span class="hljs-attr">apiKeys:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">${self:custom.base}-key</span>
  <span class="hljs-attr">substitutions:</span>
    <span class="hljs-attr">accountId:</span>
      <span class="hljs-attr">Ref:</span> <span class="hljs-string">AWS::AccountId</span>
    <span class="hljs-attr">queueName:</span> <span class="hljs-string">decoupling-sqs</span>
<span class="hljs-string">...</span>
</code></pre>
<p>Some key insights from the above configuration:</p>
<ul>
<li><p>Cloudwatch can end up being expensive, but to avoid racking up a high bill we configured them to only be retained for one day. We kept the log level to <code>ALL</code> to ensure we can see all logs during debugging, but make sure to lower that for any production projects, AppSync Logs are very verbose.</p>
</li>
<li><p>Multiple authentication methods, we want two different auth methods to ensure that: Our API is private and that we can limit who can trigger the <code>streamedResponse</code> mutation.</p>
</li>
<li><p>Substitutions: AppSync resolvers don't support environment variables, a workaround for that would be to use substitutions. This feature will act as environment variables by substituting some mock text in the resolver code with the actual required values.</p>
</li>
</ul>
<h2 id="heading-datasources-and-resolvers">DataSources and Resolvers</h2>
<p>Apart from the above API configuration we also need to ensure we configure the code that will resolve each operation and the different data sources used by them.</p>
<pre><code class="lang-yaml"><span class="hljs-string">...</span>
  <span class="hljs-attr">dataSources:</span>
    <span class="hljs-attr">localResolverDS:</span>
      <span class="hljs-attr">type:</span> <span class="hljs-string">"NONE"</span>
    <span class="hljs-attr">sqsDS:</span>
      <span class="hljs-attr">type:</span> <span class="hljs-string">"HTTP"</span>
      <span class="hljs-attr">config:</span>
        <span class="hljs-attr">endpoint:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">https://sqs.${AWS::Region}.amazonaws.com/</span>
        <span class="hljs-attr">iamRoleStatements:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">"Allow"</span>
            <span class="hljs-attr">Action:</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">"sqs:*"</span>
            <span class="hljs-attr">Resource:</span>
              <span class="hljs-attr">Fn::GetAtt:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">MyQueue</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">Arn</span>
        <span class="hljs-attr">authorizationConfig:</span>
          <span class="hljs-attr">authorizationType:</span> <span class="hljs-string">AWS_IAM</span>
          <span class="hljs-attr">awsIamConfig:</span>
            <span class="hljs-attr">signingRegion:</span>
              <span class="hljs-attr">Ref:</span> <span class="hljs-string">AWS::Region</span>
            <span class="hljs-attr">signingServiceName:</span> <span class="hljs-string">sqs</span>
  <span class="hljs-attr">resolvers:</span>
    <span class="hljs-attr">Mutation.sendPrompt:</span>
      <span class="hljs-attr">kind:</span> <span class="hljs-string">UNIT</span>
      <span class="hljs-attr">dataSource:</span> <span class="hljs-string">sqsDS</span>
      <span class="hljs-attr">code:</span> <span class="hljs-string">"./src/appsync/sendPrompt.js"</span>
    <span class="hljs-attr">Mutation.streamedResponse:</span>
      <span class="hljs-attr">kind:</span> <span class="hljs-string">UNIT</span>
      <span class="hljs-attr">dataSource:</span> <span class="hljs-string">localResolverDS</span>
      <span class="hljs-attr">code:</span> <span class="hljs-string">"./src/appsync/streamedResponse.js"</span>
</code></pre>
<p>Key takeaways from this config are:</p>
<ul>
<li><p>Data Sources: What Resolvers use to fetch data and resolve operations. In this case, we configure two different types.</p>
<ul>
<li><p><code>NONE</code>: Used for resolvers that only rely on local business logic, without the need to retrieve/send any information</p>
</li>
<li><p><code>HTTP</code>: Not all AWS services are supported to integrate directly with AppSync, but if it has an HTTP endpoint, you can trigger it via HTTP request. For example, SQS.</p>
</li>
</ul>
</li>
<li><p>Resolvers: In this section, we define what kind, data source and code will be used to resolve a specific operation or data type.</p>
</li>
</ul>
<h2 id="heading-decoupling-lambda">Decoupling Lambda</h2>
<p>Once we have the API up and running, we can focus on how to configure a Lambda function to process all messages from the SQS Queue.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">functions:</span>
  <span class="hljs-attr">sqsHandler:</span>
    <span class="hljs-attr">handler:</span> <span class="hljs-string">src/decoupled.handler</span>
    <span class="hljs-attr">role:</span> <span class="hljs-string">LambdaRole</span>
    <span class="hljs-attr">logRetentionInDays:</span> <span class="hljs-number">1</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">GRAPHQL_ENDPOINT:</span> { <span class="hljs-attr">Fn::GetAtt:</span> [<span class="hljs-string">GraphQlApi</span>, <span class="hljs-string">GraphQLUrl</span>] }
      <span class="hljs-attr">REGION:</span>
        <span class="hljs-attr">Ref:</span> <span class="hljs-string">AWS::Region</span>
    <span class="hljs-attr">events:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">sqs:</span>
          <span class="hljs-attr">arn:</span>
            <span class="hljs-attr">Fn::GetAtt:</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">MyQueue</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">Arn</span>
          <span class="hljs-attr">batchSize:</span> <span class="hljs-number">1</span>
</code></pre>
<p>This configuration is not different than any other Lambda Function triggered by a SQS Queue. But there are still some takeaway points from this configuration:</p>
<ul>
<li><p>IAM Role: Developers will need to add and configure a custom IAM role for this Lambda role to be able to sign requests to AppSync.</p>
</li>
<li><p>Log retention: Similar to the AppSync configuration, we want to limit the time that the logs are stored, in this case, the logs should be deleted after one day.</p>
</li>
<li><p>AppSync API Endpoint: Something that developers can struggle with is getting the URL endpoint from the AppSync API generated in the same <code>serverless.yml</code>. To get that value one could use <code>{ Fn::GetAtt: [GraphQlApi, GraphQLUrl] }</code> to resolve it during deployment.</p>
</li>
</ul>
<h2 id="heading-implementing-the-code">Implementing the code</h2>
<p>The code to complete the above example configuration can be found on the provided <a target="_blank" href="https://github.com/Lorenzohidalgo/appsync-decoupling-sample">Github Repository</a>, but the following is an example of one of the trickiest parts.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { util } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-appsync/utils"</span>;

<span class="hljs-keyword">const</span> accountId = <span class="hljs-string">"#accountId#"</span>;
<span class="hljs-keyword">const</span> queueName = <span class="hljs-string">"#queueName#"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">request</span>(<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">const</span> { userPrompt } = ctx.args;

  <span class="hljs-keyword">const</span> msgBody = {
    ...userPrompt,
    <span class="hljs-attr">messageId</span>: util.autoId(),
  };

  ctx.stash.msgBody = msgBody;

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">version</span>: <span class="hljs-string">"2018-05-29"</span>,
    <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
    <span class="hljs-attr">resourcePath</span>: <span class="hljs-string">`/<span class="hljs-subst">${accountId}</span>/<span class="hljs-subst">${queueName}</span>`</span>,
    <span class="hljs-attr">params</span>: {
      <span class="hljs-attr">body</span>: <span class="hljs-string">`Action=SendMessage&amp;Version=2012-11-05&amp;MessageBody=<span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(
        msgBody
      )}</span>`</span>,
      <span class="hljs-attr">headers</span>: {
        <span class="hljs-string">"content-type"</span>: <span class="hljs-string">"application/x-www-form-urlencoded"</span>,
      },
    },
  };
}
</code></pre>
<p>The code sample is part of the JS resolver configured for the <code>sendPrompt</code> mutation. As part of this sample, we can learn:</p>
<ul>
<li><p>JS Resolver substitutions: When using substitutions with JS resolvers, developers need to make sure they define a variable <code>const accountId = "#accountId#";</code> where the value will be replaced with the value provided in the configuration with the same name as the one between the <code>#</code>.</p>
</li>
<li><p>Building a <code>HTTP</code> request: The returned object by the <code>request</code> function is an example of how to build an HTTP request for accessing/triggering the SQS API.</p>
</li>
</ul>
<h1 id="heading-conclusions">Conclusions</h1>
<p>In conclusion, AWS AppSync Subscriptions can effectively decouple long-running tasks from front-end requests in serverless chat applications.</p>
<p>By implementing the two-step process of decoupling and processing with notifications, developers can enhance user experience and avoid request timeouts.</p>
<p>The provided sample code and repository offer a practical guide to implementing this solution, showcasing the use of GraphQL schema, AppSync API configuration, data sources, resolvers, and Lambda function setup.</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p>Sample Github Repository: <a target="_blank" href="https://github.com/Lorenzohidalgo/appsync-decoupling-sample">https://github.com/Lorenzohidalgo/appsync-decoupling-sample</a></p>
</li>
<li><p>Serverless AppSync Repository: <a target="_blank" href="https://github.com/sid88in/serverless-appsync-plugin">https://github.com/sid88in/serverless-appsync-plugin</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[[Webinar] Serverless Holiday Hackathon - LLM Crash Course]]></title><description><![CDATA[https://www.youtube.com/watch?v=QHaQB-9Vf9k]]></description><link>https://lhidalgo.dev/webinar-serverless-holiday-hackathon-llm-crash-course</link><guid isPermaLink="true">https://lhidalgo.dev/webinar-serverless-holiday-hackathon-llm-crash-course</guid><category><![CDATA[AWS]]></category><category><![CDATA[llm]]></category><category><![CDATA[webinar]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Fri, 01 Dec 2023 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710518231559/ef007779-6b7c-41dd-a6e6-f9d046392792.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=QHaQB-9Vf9k">https://www.youtube.com/watch?v=QHaQB-9Vf9k</a></div>
]]></content:encoded></item><item><title><![CDATA[[Webinar] Serverless Holiday Hackathon 101]]></title><description><![CDATA[https://www.youtube.com/watch?v=h3yvOk9V8dU]]></description><link>https://lhidalgo.dev/webinar-serverless-holiday-hackathon-101</link><guid isPermaLink="true">https://lhidalgo.dev/webinar-serverless-holiday-hackathon-101</guid><category><![CDATA[AWS]]></category><category><![CDATA[webinar]]></category><category><![CDATA[llm]]></category><category><![CDATA[hackathon]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Fri, 24 Nov 2023 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710518567016/ed654317-0330-4d9b-9515-2f89ffd24bc8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=h3yvOk9V8dU">https://www.youtube.com/watch?v=h3yvOk9V8dU</a></div>
]]></content:encoded></item><item><title><![CDATA[Getting Started with AppSync JavaScript Resolvers: Basic Setup]]></title><description><![CDATA[TL;DR: This article provides a step-by-step guide to setting up a new AppSync JS Resolvers project, including ESLint configuration, unit testing, and debugging. Learn how to use the AWS CLI command for testing resolver code, understand AppSync logs, ...]]></description><link>https://lhidalgo.dev/getting-started-with-appsync-javascript-resolvers-basic-setup</link><guid isPermaLink="true">https://lhidalgo.dev/getting-started-with-appsync-javascript-resolvers-basic-setup</guid><category><![CDATA[AppSync]]></category><category><![CDATA[AWS AppSync Introduction]]></category><category><![CDATA[js]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless framework]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Tue, 31 Oct 2023 13:19:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/y5_mFlLMwJk/upload/6711c1e5ef45203a2553803544febe8c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>TL;DR: This article provides a step-by-step guide to setting up a new AppSync JS Resolvers project, including ESLint configuration, unit testing, and debugging. Learn how to use the AWS CLI command for testing resolver code, understand AppSync logs, and explore best practices for writing comprehensive unit tests.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>In the first article of this series, <a target="_blank" href="https://blog.lhidalgo.dev/mastering-the-basics-of-appsync-js-resolvers-a-quick-start">Mastering the Basics of AppSync JS Resolvers: A Quick Start</a>, I shared the key insights I wish I had received before embarking on AppSync JS Resolvers.</p>
<p>In this article, as a follow-up to the first one, we will dive deeper and provide detailed steps to set up a new project and what the first steps would look like.</p>
<h1 id="heading-eslint-setup">ESLint setup</h1>
<p>For new joiners, ESLint is a static code analysis tool for identifying problematic patterns found in JavaScript code.</p>
<p>As we already know, the APPSYNC_JS runtime has some limitations, <a target="_blank" href="https://blog.lhidalgo.dev/mastering-the-basics-of-appsync-js-resolvers-a-quick-start#heading-not-everything-is-allowed">where not everything is allowed</a>. There are some JS operations that are not supported and can't be used while developing JS Resolvers (for example: try-catch, async/await, third-party libraries, ...).</p>
<p>Thankfully, the AppSync team published an <a target="_blank" href="https://www.npmjs.com/package/@aws-appsync/eslint-plugin">ESLint plugin</a> to allow users to detect those unsupported operations and solve them before trying to push the code.</p>
<p>The provided plugin is fairly limited in what it will allow you to use and it was intended to be used only for developing we recommend to only set it up for the folder containing the resolver code.</p>
<h2 id="heading-general-configuration">General Configuration</h2>
<p>As for the desired general ESLint configuration, you can go ahead and configure everything like you're already familiar with.</p>
<p>For our example, we went with installing:</p>
<ul>
<li><p>ESLint</p>
</li>
<li><p>Prettier</p>
</li>
<li><p>airbnb-base plugin</p>
</li>
</ul>
<p>All your configurations can be at the root of the project, you just need to take into consideration that whatever you configure at the root will affect all files in the project.</p>
<h2 id="heading-configuration-for-resolvers">Configuration for Resolvers</h2>
<p>In order to also apply the ESLint plugin provided by AppSync but limit it to only the resolver code, you will need to:</p>
<ul>
<li><p>Create a folder where you will store all the AppSync Resolver code</p>
</li>
<li><p>Add a new <code>.eslintrc</code> config file in that folder</p>
</li>
</ul>
<p>That new config file could be like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"extends"</span>: [<span class="hljs-string">"plugin:@aws-appsync/base"</span>]
}
</code></pre>
<p>If you only need or want to implement the base linting configuration. Once configured, you will be able to see the linting issues detected by the different plugins.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695031849215/b455f4b2-cce8-47ff-a171-98fe7b273f22.png" alt class="image--center mx-auto" /></p>
<p>In this case, we can see the following:</p>
<ul>
<li><p><code>Try statements are not supported</code> - detected by the <code>aws-appsync</code> ESLint plugin, since try-catch statements are not allowed in JS resolvers.</p>
</li>
<li><p><code>Unexpected console statement</code> - detected by the default general rules that were applied project-wise in the prior step.</p>
</li>
</ul>
<p>As always, I highly recommend you go through the <a target="_blank" href="https://www.npmjs.com/package/@aws-appsync/eslint-plugin">official documentation</a> to check all the different configuration options that are available.</p>
<h1 id="heading-unit-tests">Unit Tests</h1>
<p>Following best practices, you should always aim to write extensive and comprehensive unit tests to ensure that the developed features work as expected and consequent updates don't break existing functionality.</p>
<p>When writing unit tests for JS resolvers you might find different complications that you might or might not know how to solve.</p>
<p>Here are the solutions that we found for two of the most common requirements (for <code>jest</code> testing framework).</p>
<h2 id="heading-mocking-aws-appsyncutils">Mocking <code>@aws-appsync/utils</code></h2>
<p>As of right now, there isn't an easy way to mock <code>@aws-appsync/utils</code> or test resolvers using that dependency using jest.</p>
<h2 id="heading-mocking-runtime-global">Mocking <code>runtime</code> global</h2>
<p>While developing Javascript resolvers, one might also end up using <code>runtime</code> functionalities, for example, <code>runtime.earlyReturn(...)</code> to break the current execution.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">request</span>(<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">const</span> { <span class="hljs-attr">stash</span>: { triggerEarlyReturn } } = ctx;

  <span class="hljs-keyword">if</span> (triggerEarlyReturn) {
    runtime.earlyReturn({});
  }

  <span class="hljs-comment">// Some logic that shouldn't be executed</span>
  <span class="hljs-keyword">const</span> result = { ... }
  <span class="hljs-keyword">return</span> result;
}
</code></pre>
<p>If not handled properly, all unit tests written for those resolvers will fail directly, as these runtime functions are not defined in your local runtime environment.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> {
  request,
} = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./myResolver'</span>);

describe(<span class="hljs-string">'My test set'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> earlyReturnMock = jest.fn();

  beforeAll(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">global</span>.runtime = {
      <span class="hljs-attr">earlyReturn</span>: earlyReturnMock,
    };
    earlyReturnMock.mockImplementation(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'earlyReturn'</span>);
    });
  });

  test(<span class="hljs-string">'Happy Path Test'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> ctx = { ... };
    <span class="hljs-keyword">const</span> expectedResult = { ... };
    <span class="hljs-keyword">const</span> result = request(ctx);
    expect(result).toEqual(expectedResult);
    expect(earlyReturnMock).not.toHaveBeenCalled();
  });

  test(<span class="hljs-string">'Early Return Test'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> ctx = { ... };
    expect(<span class="hljs-function">() =&gt;</span> request(ctx)).toThrow(<span class="hljs-string">'earlyReturn'</span>);
    expect(earlyReturnMock).toHaveBeenCalledTimes(<span class="hljs-number">1</span>);
  });
});
</code></pre>
<p>By mocking and enabling it as shown in the example, the tests will stop breaking and one will be able to further test the expected behaviours.</p>
<h1 id="heading-debugging-resolvers">Debugging Resolvers</h1>
<p>One of the biggest pain points I faced when I started developing resolvers was how one could be able to debug them and find possible issues before they actually broke the CI/CD pipeline.</p>
<p>Here are a few insights on how one can do it with only the use of official AWS services.</p>
<h2 id="heading-aws-cli-command">AWS CLI command</h2>
<p>The AWS CLI can be used to test resolver code by executing the following command:</p>
<pre><code class="lang-bash">aws appsync evaluate-code \
  --code file://resolver.js \
  --<span class="hljs-keyword">function</span> request \
  --context file://context.json \
  --runtime name=APPSYNC_JS,runtimeVersion=1.0.0
</code></pre>
<p>Where, <code>aws appsync evaluate-code</code> tells the CLI the action that you'd like to do (in this case, evaluating the code for a resolver) and where one will need to provide the following input arguments:</p>
<ul>
<li><p><code>--code</code>: This argument is used to point to the code that one wants to test. the prefix <code>file://</code> should be used when pointing to local files.</p>
</li>
<li><p><code>--function</code>: This argument defines the function to be evaluated, the possible options are either <code>request</code> or <code>response</code></p>
</li>
<li><p><code>--context</code>: This argument is used to provide the context object that will be used to call your code.</p>
</li>
<li><p><code>--runtime</code>: This argument is used to select the runtime that will be used to evaluate the code. For JS resolvers one should always use <code>name=APPSYNC_JS,runtimeVersion=1.0.0</code></p>
</li>
</ul>
<p>One could expect the response of the execution to be in the following format:</p>
<pre><code class="lang-json">{
   <span class="hljs-attr">"error"</span>: { 
      <span class="hljs-attr">"codeErrors"</span>: [ 
         { 
            <span class="hljs-attr">"errorType"</span>: <span class="hljs-string">"string"</span>,
            <span class="hljs-attr">"location"</span>: { 
               <span class="hljs-attr">"column"</span>: number,
               <span class="hljs-attr">"line"</span>: number,
               <span class="hljs-attr">"span"</span>: number
            },
            <span class="hljs-attr">"value"</span>: <span class="hljs-string">"string"</span>
         }
      ],
      <span class="hljs-attr">"message"</span>: <span class="hljs-string">"string"</span>
   },
   <span class="hljs-attr">"evaluationResult"</span>: <span class="hljs-string">"string"</span>,
   <span class="hljs-attr">"logs"</span>: [ <span class="hljs-string">"string"</span> ]
}
</code></pre>
<p>The values that one could expect for each attribute are:</p>
<ul>
<li><p><code>error</code>: When provided in the response, this key will provide the details for the error thrown by the evaluation of the uploaded code.</p>
</li>
<li><p><code>evaluationResult</code>: This key will contain the response of the evaluated function (whatever the function returns) in string format, meaning that if an object is returned, one will receive the stringified value of the same.</p>
</li>
<li><p><code>logs</code>: This key will provide a list of all the generated logs during the code evaluation. This is especially useful to test/check for the output of any <code>console.log</code> used in the code.</p>
</li>
</ul>
<p>Please review the <a target="_blank" href="https://awscli.amazonaws.com/v2/documentation/api/latest/reference/appsync/evaluate-code.html">Official CLI command documentation</a> or <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/test-debug-resolvers-js.html">the official debugging guide</a> for a more detailed description.</p>
<h2 id="heading-sending-requests">Sending Requests</h2>
<p>The last step of testing and debugging your code will be to execute some End-to-End testing of your whole AppSync API.</p>
<p>In order to do this one could choose from a multitude of different tools to build and execute or send requests.</p>
<p>Here is a list of my recommended ways to do so:</p>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/retrieve-data-with-graphql-query.html">AWS AppSync console</a>: The built-in and probably easiest way of building and sending requests. My favourite feature of this approach is the built-in method to build the requests by selecting and providing only the required fields.</p>
</li>
<li><p><a target="_blank" href="https://graphbolt.dev/">Graphbolt</a>: My go-to tool when it comes to AppSync requests. Graphbolt allows you to test, debug and monitor your AppSync APIs from within the same UI. It even shows the Cloudwatch logs and X-Ray traces for each request!</p>
</li>
<li><p><a target="_blank" href="https://www.postman.com/">Postman</a>: There is probably no description needed here, but this would be the go-to if you're working on different API types at the same time or need to be able to easily share collections with your team.</p>
</li>
</ul>
<h1 id="heading-understanding-appsync-logs">Understanding AppSync Logs</h1>
<p>One of the most challenging parts of debugging AppSync could be understanding the logs and all the information provided in them.</p>
<p>I highly recommend the latest article from <a class="user-mention" href="https://hashnode.com/@bboure">Benoît Bouré</a> where he explains <a target="_blank" href="https://blog.graphbolt.dev/debugging-aws-appsync-apis-with-cloudwatch">Debugging AWS Appsync APIs with Cloudwatch logs</a>.</p>
<p>As a little easter egg, make sure to check the last section of the blog post, where he goes over how one could use <a target="_blank" href="https://graphbolt.dev/">Graphbolt</a> to make this process even faster and easier to understand.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Between the last article <a target="_blank" href="https://blog.lhidalgo.dev/mastering-the-basics-of-appsync-js-resolvers-a-quick-start">Mastering the Basics of AppSync JS Resolvers: A Quick Start</a> and this one, we provided the most relevant information and first steps developers need to consider when getting started with an AppSync API that uses JS resolvers.</p>
<p>That said, now is the point for every reader to start developing its own API in order to better understand how AppSync works and the benefits that it could bring to your organization.</p>
<p>One of the biggest pain points that we have faced so far when developing for AppSync or GraphQL APIs with constantly changing schemas has been to generate or keep the collections updated.</p>
<p>In the next article, we will go over how the package <a target="_blank" href="https://www.npmjs.com/package/gql-request-generator">gql-request-generator</a> was developed and how one could use it (or even integrate it in a CICD pipeline) to easily keep all Collections up to date.</p>
]]></content:encoded></item><item><title><![CDATA[Using AWS X-RAY to improve Serverless performance]]></title><description><![CDATA[Building high performance serverless applications can be tough, but a service like AWS X-Ray will help you understand your AWS Lambda application code better, slow DynamoDB queries or HTTPS requests, and then track how your changes improve over time....]]></description><link>https://lhidalgo.dev/using-aws-x-ray-to-improve-serverless-performance</link><guid isPermaLink="true">https://lhidalgo.dev/using-aws-x-ray-to-improve-serverless-performance</guid><category><![CDATA[AWS]]></category><category><![CDATA[xray]]></category><category><![CDATA[observability]]></category><category><![CDATA[lambda]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Wed, 19 Jul 2023 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710424966727/bddb2ce1-2936-453f-89a6-5e407ba22696.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building <strong>high performance serverless applications</strong> can be tough, but a service like <strong>AWS X-Ray</strong> will help you understand your <strong>AWS Lambda</strong> application code better, <strong>slow DynamoDB queries</strong> or HTTPS requests, and then track how your changes improve over time.</p>
<p>AWS X-Ray is a service that collects data from all the requests handled by your services, allowing you to visualize and analyze it. It generates service maps, response time or duration distribution, and segment timelines to help developers debug performance issues and improve the overall performance of their code. Setting up X-Ray is straightforward and only requires a few simple steps, including enabling tracing and configuring what X-Ray should capture.</p>
<h1 id="heading-introduction">Introduction</h1>
<p>Observability and code profiling is, at least in most cases, an afterthought that comes into play once it’s too late. Most developers start thinking about it when they face issues, timeouts, or bottlenecks that they can’t easily resolve or justify with only the execution logs.</p>
<p>There are a lot of third-party observability and profiling third-party tools out there that might be useful, for example, <a target="_blank" href="https://docs.sentry.io/platforms/node/guides/aws-lambda/profiling/?utm_m=&amp;utm_source=blog">Sentry.io</a>, <a target="_blank" href="https://www.dynatrace.com/solutions/full-stack-monitoring/">Dynatrace</a>, or <a target="_blank" href="https://www.datadoghq.com/product/apm/">DataDog</a>. But if you want to stay inside the AWS ecosystem, and have an almost effortless integration, <a target="_blank" href="https://docs.aws.amazon.com/xray/latest/devguide/aws-xray.html">AWS X-Ray</a> would be the tool to choose.</p>
<p>In this article, we will go over what AWS X-Ray is, how we can enable it, and how it will help us to better understand and debug the performance of our code.</p>
<h1 id="heading-what-is-aws-x-ray">What is AWS X-Ray?</h1>
<p>As per AWS's words, “AWS X-Ray is a service that collects data from all the requests handled by your services and allows you to visualize and analyze it.” In other words, AWS X-Ray is for requests and services interaction what Cloudwatch is for execution logs.</p>
<p>When enabled and correctly configured, AWS X-Ray will collect and measure all service interactions for a specific request. This information will be stored and made available for analysis in the following ways:</p>
<h2 id="heading-1-service-maps">1. Service Maps</h2>
<p><strong>AWS X-Ray</strong> generates service maps to allow for a visual understanding of how all different services interact with each other. Using a somewhat simple CRUD orders API as an example; we’d be able to see the following two service maps:</p>
<h3 id="heading-global-service-map">Global Service Map:</h3>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/654501de56a77e22260138b2_aws-x-ray-diagram-trace-map.webp" alt="An AWS X-Ray trace map" /></p>
<p>This type of service map will allow the user to understand and visualize how all the services under the same domain interact. In this screenshot, we can see that the end client interacts with a single API Gateway which, depending on the request type, depends on a set of Lambda functions to process the request and interact with a single DynamoDB table.</p>
<h3 id="heading-trace-service-map">Trace Service Map:</h3>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65450219c56d4f85f3b56a8c_aws-x-ray-trace-map.webp" alt="An AWS X-Ray trace between client, API Gateway, and AWS Lambda" /></p>
<p>The second kind of service map can be found on the console when reviewing a single trace and will contain only the interactions with services for that single trace or user request. This is the one that will help us the most when trying to debug performance issues for different features since we’ll be able to see what services are being used and how the different executions took place.</p>
<p>For example, in the provided screenshot we’re able to see that the execution failed since the nodes displayed for both API Gateway and AWS Lambda are in an error state.</p>
<h2 id="heading-2-response-time-or-duration-distribution">2. Response Time or Duration Distribution</h2>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/6545029f26a92d8fe031d0f1_aws-x-ray-metrics-chart-duration.webp" alt="An AWS X-Ray chart showing response time duration" /></p>
<p><strong>AWS X-Ray</strong> also provides a distribution diagram for groups of traces. This is specifically useful when trying to understand the overall performance of your service, how it affects the end users, and how much effort should be put into improving the service for each scenario.</p>
<p>For example, performance issues should be prioritized differently if it affects only 0.5% of all requests or if it affects 50% of them.</p>
<h2 id="heading-3-segment-timelines">3. Segment Timelines</h2>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/654502d6fa5466ac813aeea6_aws-x-ray-segment-timeline.webp" alt="An AWS X-Ray segment timeline of serverless rest api" /></p>
<p><strong>Segment timelines</strong> are the most detailed diagrams that X-Ray will provide and allow developers to understand exactly how and when each service is being used.</p>
<p>These timelines are specifically useful when debugging performance issues. For example, in this provided example we can see that the culprit for the long execution time is the Lambda Initialization (a.k.a. Cold Start) which took 859ms to complete, and that most of the execution time for the <strong>AWS Lambda</strong> invocation itself was spent requesting the deletion of the item in <strong>DynamoDB</strong>.</p>
<h1 id="heading-enable-aws-x-ray-in-aws-lambda">Enable AWS X-Ray in AWS Lambda</h1>
<p>Enabling AWS X-Ray on your AWS Lambda functions is very straightforward, and can normally be accomplished within a few minutes following two simple steps.</p>
<h2 id="heading-step-1-enable-aws-x-ray-tracing-on-your-services">Step 1: Enable AWS X-Ray tracing on Your Services</h2>
<p>The easiest way to do so is to enable it from your IaC template. For example, when using <strong>Serverless Framework</strong>, you can just add the following attributes under <strong>'provider.tracing'</strong> to enable AWS X-Ray on all your defined AWS Lambda functions and the generated API Gateway.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/6545033cc26611a9495107d5_code-snippet-serverless-framework-tracing-enabled.webp" alt="A code snippet of serverless framework enabling tracing on Lambda and API Gateway" /></p>
<p>Another option, in case you’re not deploying your services with an IaC template, would be to enable it manually through the AWS Console.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/6545039b00c83f9050382e51_aws-console-enabling-x-ray.webp" alt="Setting up AWS X-Ray tracing in the AWS Console" /></p>
<p>You will just need to head over to your Lambda configuration section and Edit the <strong>'Monitoring tools'</strong> section by enabling the tracing switch.</p>
<h2 id="heading-step-2-configure-what-x-ray-should-capture">Step 2: Configure What X-Ray Should Capture</h2>
<p>After enabling <strong>AWS X-Ray</strong> on your Lambda function, you will also need to add the <strong>'aws-xray-sdk-core'</strong> library to your project's dependencies and configure it to add the required traces.</p>
<h3 id="heading-capturing-aws-sdk-usage">Capturing AWS SDK Usage:</h3>
<p>Wrapping the <strong>'aws-sdk client'</strong> with the showcased functions will allow AWS X-Ray to capture and trace how the client is being used. For a Node JS project, there are two different configurations depending on the aws-sdk version your project is currently using.</p>
<p>If you are using aws-sdk v2, you'll only need to wrap the library once and it will have a “global” effect.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/654503ecc179ea09c9aeb91d_nodejs-enabling-x-ray.webp" alt="A code snippet showing setting up AWS Lambda with AWS X-Ray tracing" /></p>
<p>For example, with the provided code snippet, AWS X-Ray will be able to capture all requests done with the v2 SDK, independently of the client (SSM, DynamoDB, S3, etc).</p>
<p>When using the v3 SDK, the developer will need to add the AWS X-Ray wrapper to all the instantiated clients.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/6545049541aebf71c961716c_nodejs-lambda-x-ray-dyanmodb-tracing.webp" alt="A code snippet showing how to trace Amazon DynamoDB" /></p>
<p>This snippet, for example, will allow AWS X-Ray to only capture the requests made with that specific DynamoDB client. If you instantiate another DynamoDB client (or for any other service) in another file without adding the wrapper, AWS X-Ray won’t be able to capture the usage.</p>
<h3 id="heading-capturing-https-requests">Capturing HTTPS Requests:</h3>
<p>In some cases, your project may also use some HTTPS requests to, for example, access third-party APIs which you’d also like to capture.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/6545050ab52131bd4bc7daf2_nodejs-lambda-tracing-https.webp" alt="A code snippet showing how to trace HTTPS requests with AWS X-Ray" /></p>
<p>In order to do so, and similarly to wrapping the v2 SDK, you can add the above snippet to your index file to allow AWS X-Ray to capture all the HTTPS requests made by your code.</p>
<h3 id="heading-adding-custom-subsegments">Adding Custom Subsegments:</h3>
<p>Custom subsegments will allow AWS X-Ray to capture and measure the execution time of the desired part of your code.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/6545048941aebf71c9616c40_nodejs-lambda-segmenting-example.webp" alt="A code snippet showing an example of custom subsegments" /></p>
<p>One may add a custom segment by following the provided code snippet, where AWS X-Ray will measure the execution time of the code written between the <strong>'segment.addNewSubsegment(…)'</strong> and <strong>'subsegment.close()'</strong> functions. The custom subsegments will be displayed in the AWS X-Ray console under the Segment Timelines diagram.</p>
<h1 id="heading-how-can-aws-x-ray-help-you-find-performance-bottlenecks">How Can AWS X-Ray Help You Find Performance Bottlenecks?</h1>
<p>Now that we know what AWS X-Ray has to offer and how to set it up, most of you will already have guessed how it can be a very useful tool but here are my favorite ways to take advantage of it:</p>
<h2 id="heading-discovering-critical-services-under-the-same-domain">Discovering Critical Services Under the Same Domain</h2>
<p><strong>AWS X-Ray</strong> provides (and builds based on the selected traces) a visual map of all the services linked to a specified domain.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/654501de56a77e22260138b2_aws-x-ray-diagram-trace-map.webp" alt="A trace map of a serverless REST API using AWS X-Ray" /></p>
<p>In the example used above, we can see that this domain is composed of one API Gateway, four AWS Lambdas, and a single DynamoDB table.</p>
<p>Apart from seeing all the services linked to a domain, AWS X-Ray will also visually display the percentage of error executions that nodes might have.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/6545061d5da6f8685fe95a1f_x-ray-trace-map-client-to-appsync.webp" alt="A trace map of AWS AppSync" /></p>
<p>In this provided example, we can easily see that the <strong>AppSync GraphQL API</strong> has an average error rate of around 10%.</p>
<h2 id="heading-identifying-the-performance-bottlenecks">Identifying the Performance Bottlenecks</h2>
<p>Once the developer has found the nodes to be analyzed, the next step would be taking a look at the different traces for that node to better understand where it’s spending most of the execution time.</p>
<p>For this task, one could take advantage of the Segment Timelines. These timelines will visually display how long each action took to be executed.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/6545066ca2f331272701826b_x-ray-lambda-segment-timeline-2.webp" alt="A segment timeline showing AWS Lambda" /></p>
<p>From this example, we could see that the bottlenecks for the execution of this Lambda were:</p>
<ol>
<li><p>The cold start, which added 733ms to the execution time.</p>
</li>
<li><p>The API request, which took 5.53s of the actual invocation time of the lambda.</p>
</li>
</ol>
<p>After seeing these results, a developer would know that he would need to work possibly on avoiding cold starts and on improving (if owned) the API that is called during the execution.</p>
<h2 id="heading-using-custom-subsegments-to-profile-amp-improve-your-code">Using Custom Subsegments to Profile &amp; Improve Your Code</h2>
<p>At last, one of the best-kept secrets of AWS X-Ray, using custom subsegments to profile your code and have a better understanding of where the time is spent during an execution.</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/654506b4f09d338503279844_x-ray-lambda-segment-timeline-3.webp" alt="A segment timeline with a custom subsegment to show a time consuming operation" /></p>
<p>Given this Segment Timeline, we would know that the execution is taking longer than it should since we expected the DynamoDB request to be the only time-consuming operation. Without the above traces, we wouldn’t be able to understand what is taking so long.</p>
<p>At this point, a developer would have two options:</p>
<ol>
<li><p>Spend hours reviewing the code and blindly updating it to try to find the time-consuming task.</p>
</li>
<li><p>Add custom subsegments to the operations we suspect could be the culprit of the high execution time.</p>
</li>
</ol>
<p>After adding a custom subsegment to the suspected function, we would be able to see a trace like this:</p>
<p><img src="https://assets-global.website-files.com/5f16ec6886fb3fb049008f9a/65450709bb1876da6c448d24_x-ray-lambda-segment-timeline.webp" alt="A segment timeline showing after adding the custom subsegment" /></p>
<p>Here we can clearly see that the <strong>'time-consuming operation'</strong> function is responsible for the extra 2.5 seconds of execution time. Thanks to this insight, the developer would know that they only need to focus on reviewing and improving that specific operation.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>We highly recommend correctly setting up AWS X-Ray when developing services on AWS, especially if they are serverless, in order to allow for better observability and profiling. As showcased in this article, AWS X-Ray will allow developers to better understand how a service is performing and speed up the process of debugging performance bottlenecks or timeouts once the service is deployed to production.</p>
<p>Did you find this article interesting or useful? Do you have any questions or would like to chat more about it? I’d love to connect with you on my social media, you can find me on <a target="_blank" href="https://www.linkedin.com/in/lorenzo-hidalgo-gadea/">LinkedIn</a> or <a target="_blank" href="https://twitter.com/lhidalgo_dev">Twitter</a>.</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/xray/latest/devguide/aws-xray.html">https://docs.aws.amazon.com/xray/latest/devguide/aws-xray.html</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-subsegments.html">https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-subsegments.html</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Mastering the Basics of AppSync JS Resolvers: A Quick Start]]></title><description><![CDATA[TL;DR;

This article provides an overview of the basics of AppSync APIs, including the recommended stack of Serverless Framework, Serverless AppSync Plugin, and GraphBolt. It explains the structure of resolvers, the use of ctx and stash objects, size...]]></description><link>https://lhidalgo.dev/mastering-the-basics-of-appsync-js-resolvers-a-quick-start</link><guid isPermaLink="true">https://lhidalgo.dev/mastering-the-basics-of-appsync-js-resolvers-a-quick-start</guid><category><![CDATA[AppSync]]></category><category><![CDATA[aws appsync]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless framework]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Wed, 28 Jun 2023 14:33:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1687427887468/8e04e6dc-f669-4f5e-9d38-2dea0741c84d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-tldr">TL;DR;</h1>
<blockquote>
<p>This article provides an overview of the basics of AppSync APIs, including the recommended stack of Serverless Framework, Serverless AppSync Plugin, and GraphBolt. It explains the structure of resolvers, the use of ctx and stash objects, size limits for inline resolvers and resolvers uploaded to S3, and the use of variable substitutions for environment variables. It also covers how to refactor code and use it as a JS resolver for AppSync, and how to use GraphBolt for executing API requests and debugging resolvers.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>Getting started with AppSync APIs sounds like a daunting task, especially for developers used to developing REST APIs. There are a few new concepts that you'll need to learn and understand like the usage of data sources, Resolvers, Pipeline resolvers and how to set up a proper GraphQL schema.</p>
<p>In this article, we will expect that the basic knowledge is already covered, for example by heading to and reading the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/what-is-appsync.html">official documentation</a>, and we will cover what we think will allow you to get started as fast as possible while still avoiding some common mistakes.</p>
<h1 id="heading-why-javascript-resolvers"><strong>Why JavaScript Resolvers?</strong></h1>
<p>While developing AppSync APIs you have a few options to choose from to implement your logic, but we will focus on JavaScript resolvers since:</p>
<ul>
<li><p>They are the fastest way to get started</p>
</li>
<li><p>Most of the developers will already have previous JS knowledge</p>
</li>
</ul>
<blockquote>
<p>As a little disclaimer, the JS resolvers are executed in a custom runtime called <code>APPSYNC_JS</code> where not all JS operations are supported, but we'll dive into it later.</p>
</blockquote>
<p>Another option would be to go with VTL Mapping Template resolvers, but this would mean entering a rabbit hole that might be too overwhelming for developers trying to get started with AppSync.</p>
<h1 id="heading-recommended-stack">Recommended Stack</h1>
<p>With simplicity and future-proofness as our main goals, the stack to recommend would always be:</p>
<ul>
<li><p><a target="_blank" href="https://www.serverless.com/">Serverless Framework</a>: My go-to IaC framework for developing serverless apps on AWS. This framework allows for an easy and fast declaration for every infrastructure or resource your project might need, and, for the ones that aren't as easy to set up, it allows for third-party plugins to make your life easier.</p>
</li>
<li><p><a target="_blank" href="https://github.com/sid88in/serverless-appsync-plugin">Serverless AppSync Plugin</a>: Plugin used to simplify the declaration and set up of your AppSync API.</p>
</li>
<li><p><a target="_blank" href="https://graphbolt.dev/">GraphBolt</a>: The best tool I've been able to find so far to debug and test AppSync resolvers and APIs.</p>
</li>
</ul>
<p>For an example of how to get started with both Serverless Framework and the Serverless AppSync Plugin, you can head to the project's <a target="_blank" href="https://github.com/sid88in/serverless-appsync-plugin#quick-start">readme quick start guide</a>.</p>
<h1 id="heading-basic-concepts">Basic Concepts</h1>
<p>We won't go into much detail on the different concepts, but as a quick refresher of the basics:</p>
<ul>
<li><p>Data Sources: As the name implies, it is the reference to where the resolver will be fetching the data from.</p>
</li>
<li><p>Resolvers: The connector between AppSync/GraphQL and the data source, aka. the file with the logic to be executed.</p>
</li>
<li><p>Pipeline resolvers or functions: A set of resolvers to be executed in the predefined order to build the expected response.</p>
</li>
</ul>
<h1 id="heading-everything-you-need-to-know">Everything you need to know</h1>
<p>Time to go right to the point, here are all the things I wish someone had explained to me before getting started and would have saved me a lot of time and headaches.</p>
<h2 id="heading-resolver-structure-requestctx-and-responsectx">Resolver Structure: <code>request(ctx)</code> and <code>response(ctx)</code></h2>
<p>The most important thing to understand, and that you might already know, is the resolver structure. Resolvers tell AWS AppSync how to translate an incoming GraphQL request into instructions for your backend data source, and how to translate the response from that data source back into a GraphQL response.</p>
<p>Having this in mind, it starts to make sense that we're expected (and forced) to export two functions in our code:</p>
<ul>
<li><p><code>request(ctx)</code>: This will be the first function to be executed and should be used to verify the input and prepare or map the request parameters to be sent in the request made to the defined Data Source.</p>
</li>
<li><p><code>response(ctx)</code>: This function will be called after the Data Source has been called and should be used to verify the received response and to map it to the expected schema needed for the response.</p>
</li>
</ul>
<h2 id="heading-everyone-needs-a-bit-of-context">Everyone needs a bit of context</h2>
<p><code>ctx</code>, aka. context is what AppSync will provide when calling your resolver and it will contain all the information you might need to access during the execution of your resolver or pipeline.</p>
<p>The most relevant keys that you'll probably need are:</p>
<ul>
<li><p><code>ctx.args</code>: A map that contains all GraphQL request arguments.</p>
</li>
<li><p><code>ctx.stash</code>: A stash is an object that lives throughout the whole execution of the resolver or pipeline. It can be used to pass information between <code>request</code> and <code>response</code> functions from a single resolver or between different resolvers in the same pipeline.</p>
</li>
<li><p><code>ctx.result</code> and <code>ctx.error</code>: These values will contain the result or errors from this resolver. One could for example access these keys from the <code>response</code> function to access the response provided by the Data Source.</p>
</li>
<li><p><code>ctx.prev.result</code>: Contains the value of the previous operation result. F.e.: the result of the <code>response</code> function from the previously executed resolver in a pipeline.</p>
</li>
</ul>
<h2 id="heading-not-everything-is-allowed">Not everything is allowed</h2>
<p>And Appsyncs Safe word is "<em>The code contains one or more errors</em>". This is an error that you have probably already faced if you started developing JS resolvers like you were developing Lambdas.</p>
<p>JS Resolvers run in a custom runtime called <code>APPSYNC_JS</code> <strong>and is pretty limited regarding the operations that you will be able to perform inside the resolvers. To help developers avoid this kind of error, AWS provides an</strong> <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#utility-resolvers"><strong>ESLint plugin</strong></a> <strong>that can be used to</strong> statically analyze your code to quickly find problems</p>
<p>Some of the non-allowed operations that you probably wanted to use are:</p>
<ul>
<li><p><code>async</code> and <code>await</code>: These operations are not allowed, the only <code>async</code>/<code>await</code> operation that is allowed is the communication with the configured data source, which will be handled by AppSync.</p>
</li>
<li><p><code>try</code> - <code>catch</code>: You won't be able to use <code>try-catch</code> blocks, so make sure your code is top-notch and thoroughly tested.</p>
</li>
<li><p><code>import</code> and third-party dependencies: JS resolvers can't import or depend on any third-party dependency a part of <code>@aws-appsync/utils</code> which is already built into the custom runtime.</p>
</li>
</ul>
<p>These are just a subset of the non-allowed operations to see the rest of them refer to <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#utility-resolvers">the official documentation</a>.</p>
<h2 id="heading-aws-appsyncutils-your-new-best-friend"><code>@aws-appsync/utils</code>, your new best friend</h2>
<p>This library will be the only one that you'll be able to import into your code, but don't worry about it, it contains most of the helper functions that you might need for your business logic.</p>
<p>The most relevant ones that you'll probably end up using are:</p>
<ul>
<li><p><code>util.error</code> and <code>util.appendError</code>: helpers used to handle errors that you might want to throw/append to the response. <code>util.error</code> will throw an exception and break the execution and <code>util.appendError</code> will append the error and allow for the execution to continue.</p>
</li>
<li><p><code>runtime.earlyReturn</code>: This helper will break the execution of the current resolver and its input will be returned as the response. For example, calling <code>runtime.earlyReturn({});</code> in the <code>request</code> function will avoid the <code>response</code> function from being executed, but it won't break the execution of the pipeline.</p>
</li>
<li><p><code>util.dynamodb.toDynamoDB</code> and <code>util.dynamodb.toMapValues</code>: helpers that convert input objects to the appropriate DynamoDB representation.</p>
</li>
<li><p><code>util.time.*</code>: A set of helper functions to ease the handling and creation of date times.</p>
</li>
</ul>
<p>To see all of the provided operations you can check the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/built-in-util-js.html">official documentation</a>.</p>
<h2 id="heading-size-matters">Size Matters</h2>
<p>Some people might say size doesn't matter but in this context, for JS resolvers, we have some size limits that we need to follow:</p>
<ul>
<li><p>4,000 characters: This is the maximum size currently allowed for resolver code that is defined in-line in the cloud formation template. This will be your current limit if you're using the <code>serverless-appsync-plugin</code> but don't worry, there is a fix for that coming soon.</p>
</li>
<li><p>32,000 characters: In case use another IaC approach to the plugin and you're uploading the resolver code to S3, there is a higher allowed limit where you can use up to 32,000 characters per resolver.</p>
</li>
</ul>
<h2 id="heading-environment-variables-where-are-you">Environment Variables, where are you?</h2>
<p>For developers used to Lambdas, they might miss using Environment variables, especially if they change depending on the stage you deploy to.</p>
<p>AppSync/JS Resolvers don't currently support the use of environment variables but with the use of the <code>serverless-appsync-plugin</code>, you might take advantage of a feature called <a target="_blank" href="https://github.com/sid88in/serverless-appsync-plugin/blob/master/doc/substitutions.md">variable substitutions</a>.</p>
<p>To use them you will just need to add a variable in your code with the following structure:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> tableName = <span class="hljs-string">'#myTable#'</span>;
<span class="hljs-keyword">return</span> {
  <span class="hljs-attr">operation</span>: <span class="hljs-string">"BatchGetItem"</span>,
  <span class="hljs-attr">tables</span>: {
    [tableName]: { keys },
  },
};
</code></pre>
<p>The value assigned to the variables must be wrapped with <code>#</code> and the value in between should be the exact name used in your <code>serverless.yml</code> file, otherwise, it won't be updated during the deployment.</p>
<p>After setting the variable in your resolvers code, you will just need to add the substitution configuration on the <code>serverless.yml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">appSync:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">my-api</span>
  <span class="hljs-attr">substitutions:</span> <span class="hljs-comment">#global substitutions</span>
    <span class="hljs-attr">myTableName:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">myTable</span>

  <span class="hljs-attr">resolvers:</span>
    <span class="hljs-attr">Query.user:</span>
      <span class="hljs-attr">dataSource:</span> <span class="hljs-string">my-table</span>
      <span class="hljs-attr">substitutions:</span> <span class="hljs-comment">#resolver substitutions</span>
        <span class="hljs-attr">myTableName:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">myTable</span>
</code></pre>
<h2 id="heading-refactoring-is-possible-you-just-need-one-extra-step">Refactoring is possible, you just need one extra step</h2>
<p>As <a target="_blank" href="https://hashnode.com/@bboure">Benoît Bouré</a> explained in <a target="_blank" href="https://blog.graphbolt.dev/write-reusable-code-for-appsync-javascript-resolvers">his article</a> a few months ago, it's possible to write reusable/refactor code and still use it as a JS resolver.</p>
<p>To take advantage of it you will need to consider a few things:</p>
<ul>
<li><p>AppSync only supports ES modules</p>
</li>
<li><p>We will need to bundle the code before referencing it in our IaC</p>
</li>
<li><p>We don't want to bundle the AppSync utils library</p>
</li>
</ul>
<p>To bundle the code we would recommend you use the following command:</p>
<pre><code class="lang-bash">esbuild \
  --bundle \
  --target=esnext \
  --platform=node \
  --format=esm \
  --external:@aws-appsync/utils \
  --tree-shaking=<span class="hljs-literal">true</span> \
  --minify-whitespace \
  --minify-identifiers \
  --outdir=path/to/resolvers/build \
  path/to/resolvers/*.js
</code></pre>
<p>The previous command will bundle your code to the expected format to be referenced in your IaC.</p>
<blockquote>
<p>We don't recommend using the <code>--minify</code> option since we found that the underlying <code>--minify-syntax</code> tends to introduce some unsupported operations. That's why we use <code>--minify-whitespace</code> and <code>--minify-identifiers</code> sepparately.</p>
</blockquote>
<p>To automate this step, I would recommend you add the execution of the bundling as a new step to the deployment pipeline and always reference the resolvers from the build folder in your IaC.</p>
<p>A <a target="_blank" href="https://github.com/sid88in/serverless-appsync-plugin/pull/576">PR</a> to remove this extra step is currently in the making, make sure to keep an eye on the plugins changelog to see if this step is still needed as of the time you're reading this.</p>
<h2 id="heading-no-more-headaches-use-graphbolt">No more headaches, use GraphBolt</h2>
<p>Most of you will already be familiarized with Postman or any other tool that allows you to execute API requests and I know switching tools might be time-consuming, but you can think of GraphBolt as the Postman on steroids or the perfect AppSync companion.</p>
<p>The most relevant features that will help you save a lot of time and headaches are:</p>
<ul>
<li><p><a target="_blank" href="https://docs.graphbolt.dev/graphql-client/">GraphQL Client</a>: A client that allows you to execute requests. Like Postman but tailored for AppSync, allowing for almost-automatic authentication handling, autocomplete and query formatting.</p>
</li>
<li><p><a target="_blank" href="https://docs.graphbolt.dev/query-inspector/">Query Inspector</a>: This allows you to go one step ahead of Postman but without the need of leaving the app. You will be able to: see all the recently executed queries, see the details of a single query (including the X-Ray traces, Cloudwatch logs and the executed resolvers) and even debug the execution of each resolver by accessing the Resolver details. This last option will allow you to see how every <code>request</code>/<code>response</code> function has been evaluated.</p>
</li>
<li><p><a target="_blank" href="https://docs.graphbolt.dev/mapping-template-planner/code-evaluation">Resolver Evaluation</a>: As mentioned before, not everything is allowed in JS resolvers, and "<em>The code contains one or more errors</em>" is not useful at all. To streamline your development and find the issues easier you can use this function to test and evaluate the resolver code your building.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Getting started with AppSync and JS resolvers might seem like a daunting task, but by giving a quick read to the official documentation and having all the above-mentioned points in mind, you'll be up and running in no time developing AppSync GraphQL APIs.</p>
]]></content:encoded></item><item><title><![CDATA[[Webinar] Serverless Panel ¡En Esapñol!]]></title><description><![CDATA[https://www.youtube.com/watch?v=3lVOx1gKi-0]]></description><link>https://lhidalgo.dev/webinar-serverless-panel-en-esapnol</link><guid isPermaLink="true">https://lhidalgo.dev/webinar-serverless-panel-en-esapnol</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[webinar]]></category><category><![CDATA[video]]></category><dc:creator><![CDATA[Lorenzo Hidalgo Gadea]]></dc:creator><pubDate>Thu, 30 Mar 2023 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710517739969/02c8fa85-0ad9-4ad5-92a8-5495315d742b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=3lVOx1gKi-0">https://www.youtube.com/watch?v=3lVOx1gKi-0</a></div>
]]></content:encoded></item></channel></rss>