<?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[GraphBolt]]></title><description><![CDATA[A professional AppSync manager and debugger.]]></description><link>https://blog.graphbolt.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1667597192573/hajyGI-MW.png</url><title>GraphBolt</title><link>https://blog.graphbolt.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 11 Apr 2026 15:03:10 GMT</lastBuildDate><atom:link href="https://blog.graphbolt.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Asynchronous File Generation and Download with AWS AppSync, Lambda, and EventBridge]]></title><description><![CDATA[In a previous article, I explained how to generate Amazon S3 pre-signed download URLs using AWS AppSync. But what happens when the file doesn’t exist yet?
Sometimes the file must be generated on demand. Maybe it’s a report for a user-selected date ra...]]></description><link>https://blog.graphbolt.dev/asynchronous-file-generation-and-download-with-aws-appsync-lambda-and-eventbridge</link><guid isPermaLink="true">https://blog.graphbolt.dev/asynchronous-file-generation-and-download-with-aws-appsync-lambda-and-eventbridge</guid><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Sun, 15 Feb 2026 11:44:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770932336846/ce59aedd-6b57-4481-80f7-4ae8e25b134c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In a previous article, I explained how to generate <a target="_blank" href="https://blog.graphbolt.dev/how-to-download-and-upload-files-to-amazon-s3-with-aws-appsync"><strong>Amazon S3 pre-signed download URLs</strong> using AWS AppSync</a>. But what happens when the file doesn’t exist yet?</p>
<p>Sometimes the file must be generated on demand. Maybe it’s a report for a user-selected date range, a data export based on filters, or something that only makes sense at that specific moment.</p>
<p>You might be tempted to generate that file directly within the AppSync resolver (e.g. a Lambda function), store it on S3 and then return the pre-signed URL; but this would be an anti-pattern.</p>
<ul>
<li><p><strong>AppSync resolvers should stay thin</strong><br />  Resolvers are not meant to perform heavy processing. Long-running or compute-heavy tasks don’t belong there.</p>
</li>
<li><p><strong>Hard time limits</strong><br />  <a target="_blank" href="https://blog.graphbolt.dev/the-aws-appsync-limits-you-need-to-know">AppSync operations have a <strong>30-second timeout</strong></a>. File generation, especially for large datasets, can easily exceed that.</p>
</li>
<li><p><strong>Reliability &amp; retries</strong><br />  File generation can fail. You may want retries, dead-letter queues, monitoring, and proper error handling — all of which are far easier to implement asynchronously.</p>
</li>
</ul>
<p>In this article, I’ll show you how to properly decouple file generation from the AppSync request using Asynchronous Lambda invocation, Amazon EventBridge, and AppSync Subscriptions.</p>
<h2 id="heading-architecture-overview">Architecture Overview</h2>
<p>AWS AppSync supports long-running operations by allowing <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-lambda-js.html#async-invocation-type-js">asynchronous Lambda function invocations</a> as a data source (resolver). When the Lambda function is invoked asynchronously, the AppSync resolver returns immediately, while the Lambda function is allowed to run for up to 15 minutes.</p>
<p>We can leverage this capability to trigger our file-generation process without blocking the user's request.</p>
<p>Once the Lambda function finishes generating the file, it can store it in an Amazon S3 bucket, generate a pre-signed download URL, and send it back to the user through a Subscription using the <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/target-appsync.html">AppSync–EventBridge integration</a>.</p>
<p><img src="https://cdn-0.plantuml.com/plantuml/png/ZL71ZjGm3BtdAxpbCaXCk-nogX3QMGJIIY16L6ZFQUhf1DAaYfqPyVSuxGogK44SgjX-p_PxxWEnZfm6fsuwF5YFi7jkzfB3aNcakggYFfUzvMDg4s4qmJDwBaqOAdqcfrlAxzOAGDj3YDlHQPy7LgUxs_AswYonKkh6UkDIXocwwuPAtlTX688lHqb_KvPuOuVHsOF5RkQipnc5OpJRu9yyOn-dC9URdzxCZMjzidnCZDKdFRIT_ZkluH1rZzKa1YzPW_5a3JucymFvMUxpfVsEXcbbpBzbVZYXvyTeknul7hMH5P2l3PgW-2P1eIvkDdgqhw2uPB3RFnMI5Y_X1KVjQTHHfgdPqQP7Zx85Frg7mieFx6CmI1JYMjz2omQdE97G5kMrhl-wowQTAjBUtCEChirIQlBzTliEnAcI3oHS6gdXTGaxokw_8WLtmgYMUERFksguAo7BGZW8y0Uh2sSlrAlD6kvBsAVMMJh2P5XT8PZ5rF2cXC-9fz-c7j1tQN2_0000" alt="PlantUML diagram" /></p>
<p><a target="_blank" href="https://www.plantuml.com/plantuml/uml/ZL7VYzGm47xFNp5NNrPms_FEfo9oUw9meBXOv9xJTBORJPEIcUpYV--atPKYLIoK-V5zy-ERF13ho6bmwgR3W_K8k-TcBp4ScKUggohgSzbRFgOs4KOJFAFdqeIXqcTokgLijrO0sXv2t8vE-zomEjtTbhTLPucLKZVM6vSwJD5T9rHYtuTX2BuS9VnDMUAD7KPd3nQxcRFUPnIEqMo3V_ACVPpSd6v-UZCNhjN9y34nLf_qq7Rwxxo6GzGzLjCoU8aQZ2Thy36P7yZFSfylxNSqJ2jj_htcXnDwVeosw_6YKHiP0VdQe0cAR-8Kwg9ceqVx2efB1BlzKo5XyH9USDIUHXsbcveLRNhqA5FmetKeh6nW7uD1eX3NwXQxR73o4mbjaLwh-g-xRDef8-NDFSpeMY-b9jrlj-z0b2hvGToba1fUbx2ZhFuhKd0dZ6g9P_uyguQx4l9Q20S1_c1ruUo5UfLPmtq9-zGwIqUO1Aih1FCoDRmfuNFYwTcKV0_ekpIuNm00">Source</a></p>
<h2 id="heading-high-level-flow">High-level flow</h2>
<p>We use three <a target="_blank" href="https://github.com/bboure/appsync-async-file-download/blob/main/schema.gql">GraphQL operations</a>:</p>
<p><code>generateReport</code><br />This is the Mutation the user calls to express the intent to generate a file. It receives, for example, a start date and an end date and returns a <code>requestId</code> that uniquely identifies the process.</p>
<p><code>onReportGenerated</code><br />This is a Subscription that the client uses to subscribe to updates. It takes a <code>requestId</code> as input — the same <code>requestId</code> returned by the <code>generateReport</code> Mutation.<br />It is essentially a way of saying:</p>
<blockquote>
<p>“Notify me once this specific report has been generated.”</p>
</blockquote>
<p><code>reportGenerated</code><br />This is another Mutation that the backend invokes to trigger the above Subscription.</p>
<h3 id="heading-workflow">Workflow</h3>
<ol>
<li><p>The client invokes the <code>generateReport</code> Mutation.</p>
</li>
<li><p>The AppSync JS resolver generates a random <code>requestId</code>, invokes the <code>GenerateReport</code> Lambda function asynchronously, and passes it the <code>requestId</code> along with the user inputs.</p>
</li>
<li><p>AppSync immediately returns the <code>requestId</code> to the client.</p>
</li>
<li><p>The client receives the <code>requestId</code> and uses it to subscribe via <code>onReportGenerated</code>.</p>
</li>
<li><p>Once the Lambda function finishes generating the file, it stores it in an Amazon S3 bucket.</p>
</li>
<li><p>The Lambda function publishes a <code>reportGenerated</code> event to the event bus, including the <code>requestId</code> and the pre-signed download URL.</p>
</li>
<li><p>The AppSync <code>reportGenerated</code> Mutation target is triggered through the EventBridge integration.</p>
</li>
<li><p>AppSync pushes the event back to the client via the active Subscription.</p>
</li>
<li><p>The client receives the download URL and initiates the file download in the browser.</p>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can find the full implementation on <a target="_self" href="https://github.com/bboure/appsync-async-file-download/blob/main/lib/async-file-download-stack.ts">Github</a></div>
</div>

<h3 id="heading-error-handling-and-other-considerations">Error Handling and Other Considerations</h3>
<p>When using asynchronous Lambda invocations, error handling behaves differently from synchronous calls.</p>
<p>By default, if an asynchronous Lambda execution fails, AWS automatically <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-error-handling.html"><strong>retries it twice after the initial failure</strong></a> (three attempts in total). If all retry attempts fail and no additional configuration is in place, the event is discarded.</p>
<p>If you need more control over failure handling, retries, and observability, there are several options to consider:</p>
<p><strong>Lambda Destinations</strong></p>
<p>You can configure <a target="_blank" href="https://aws.amazon.com/blogs/compute/introducing-aws-lambda-destinations/">Lambda function destinations</a> for asynchronous invocations. This allows you to route failed events to another target — such as:</p>
<ul>
<li><p>An SQS queue (acting as a DLQ)</p>
</li>
<li><p>An SNS topic</p>
</li>
<li><p>EventBridge</p>
</li>
<li><p>Another Lambda function</p>
</li>
</ul>
<p>This gives you visibility into failures and the ability to reprocess or inspect failed events.</p>
<p><strong>Introduce an SQS Queue</strong></p>
<p>Instead of invoking the Lambda function asynchronously and directly from AppSync, you can <a target="_blank" href="https://blog.iamjkahn.com/posts/2019/12/invoking-even-more-aws-services-directly-from-aws-appsync">place the event onto an <strong>Amazon SQS queue</strong></a>.</p>
<p>The Lambda function then consumes messages from the queue.</p>
<p>This approach provides:</p>
<ul>
<li><p>A configurable retry policy</p>
</li>
<li><p>Built-in dead-letter queue support with redrive capability</p>
</li>
<li><p>Better control over failure handling</p>
</li>
<li><p>Buffering and backpressure management</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, we explored how to handle on-demand file generation in a clean, event-driven way using asynchronous Lambda invocations, EventBridge, and AppSync Subscriptions. Instead of blocking a GraphQL request, we decouple processing from the client interaction, allowing long-running tasks to execute reliably while still delivering real-time feedback when the file is ready.</p>
]]></content:encoded></item><item><title><![CDATA[How to download and upload files to Amazon S3 with AWS AppSync]]></title><description><![CDATA[One of the questions I’m most often asked by people new to AWS AppSync is: “How do I let my users upload or download files?”
The short answer is: you can’t — at least not directly.
While GraphQL is built on top of HTTP, it is not designed for transfe...]]></description><link>https://blog.graphbolt.dev/how-to-download-and-upload-files-to-amazon-s3-with-aws-appsync</link><guid isPermaLink="true">https://blog.graphbolt.dev/how-to-download-and-upload-files-to-amazon-s3-with-aws-appsync</guid><category><![CDATA[AWS]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[S3]]></category><category><![CDATA[S3-bucket]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Thu, 25 Dec 2025 14:02:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766351128469/55784ce3-5a5f-49f5-8efa-cbc1145cc72e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the questions I’m most often asked by people new to <strong>AWS AppSync</strong> is: <strong>“How do I let my users upload or download files?”</strong></p>
<p>The short answer is: <em>you can’t</em> — at least not directly.</p>
<p>While GraphQL is built on top of HTTP, it is <strong>not designed for transferring large binary payloads</strong>. GraphQL excels as a <strong>control plane</strong>, not as a data transport layer.</p>
<p>Fortunately, when working with AWS AppSync and the AWS ecosystem, file storage almost always means <strong>Amazon S3</strong>. And that opens the door to a much cleaner and more scalable approach: <a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html"><strong>S3 pre-signed URLs</strong></a>.</p>
<p>In this article, I’ll walk through how to use <strong>AWS AppSync as the orchestrator</strong> for secure file uploads and downloads, while letting Amazon S3 handle the heavy lifting.</p>
<h2 id="heading-high-level-architecture">🏗️ High-Level Architecture</h2>
<p>A <strong>pre-signed URL</strong> is a secure, time-limited URL that grants permission to upload or download an object from an Amazon S3 bucket. Instead of exposing AWS credentials or making your bucket public, the URL embeds a temporary signature that authorizes a specific operation (GET or PUT) on a specific object.</p>
<p>Once generated, the URL can be used <strong>directly by the client</strong> to interact with S3 in a secure and efficient way — without routing file data through your API.</p>
<p>Under the hood, a pre-signed URL is created using the <strong>AWS credentials of the entity that generates it</strong>. In the context of AWS AppSync, this is typically a Lambda function configured as an AppSync data source and resolver.</p>
<p>Those credentials define:</p>
<ul>
<li><p>Which bucket and object key can be accessed</p>
</li>
<li><p>Which operation is allowed (upload or download)</p>
</li>
<li><p>What type(s) of files can be uploaded (image, pdf, etc.)</p>
</li>
<li><p>The maximum file size permitted</p>
</li>
<li><p>How long the URL remains valid</p>
</li>
<li><p>Metadata on the object (For example, the user id of the file uploader)</p>
</li>
</ul>
<p>The overall flow looks like this:</p>
<ol>
<li><p>The client requests an upload or download URL via AppSync</p>
</li>
<li><p>AppSync validates the request (authentication, authorization, file constraints, etc.)</p>
</li>
<li><p>AppSync generates a pre-signed URL using its execution role</p>
</li>
<li><p>The client uploads or downloads the file <strong>directly from S3</strong></p>
</li>
</ol>
<p><img src="https://img.plantuml.biz/plantuml/png/hP6nRa8n34NtV8N5_gQteQe2KQbB9n0VO193ewP9S6nLzEkR007UmjGbYkwvyVLL5aMGHR-3CMWbCQo2foY01UpvPdBbtlgCLLtcl3b5soXcFq6ppGWxjLyaiuRBQCo1asRG718wSva6msjxEOSr7PMAN2ae1rFr6rwgV5QxjoW461rW5HBxNm8jn1FlEqNYvijCG_67IAwFtQu_Ul3BCFHaKOwFmVVLoZY7xGNiVI13qVKQBlt4PqKw__Sgjy5Foap27pojprjdzqQBUhOl_mC0" alt class="image--center mx-auto" /></p>
<blockquote>
<p><a target="_blank" href="https://editor.plantuml.com/uml/hP6nRa8n34NtV8N5_gQteQe2KQbB9n0VO193ewP9S6nLzEkR007UmjGbYkwvyVLL5aMGHR-3CMWbCQo2foY01UpvPdBbtlgCLLtcl3b5soXcFq6ppGWxjLyaiuRBQCo1asRG718wSva6msjxEOSr7PMAN2ae1rFr6rwgV5QxjoW461rW5HBxNm8jn1FlEqNYvijCG_67IAwFtQu_Ul3BCFHaKOwFmVVLoZY7xGNiVI13qVKQBlt4PqKw__Sgjy5Foap27pojprjdzqQBUhOl_mC0">Source code</a></p>
</blockquote>
<h2 id="heading-generate-a-pre-signed-upload-url">⬆️ Generate a pre-signed upload URL</h2>
<p>The core idea is simple: the client first asks AppSync for permission to upload a file, and AppSync responds with everything the client needs to upload <strong>directly to S3</strong>.</p>
<p>First, we need a mutation that lets the client <strong>declare its intent</strong> to upload a file.</p>
<p>Typically, the mutation needs:</p>
<ul>
<li><p>The file name</p>
</li>
<li><p>The file type (mime type)</p>
</li>
</ul>
<p>and it returns:</p>
<ul>
<li><p>the pre-signed upload URL</p>
</li>
<li><p>the fields that the client must include in the upload request</p>
</li>
</ul>
<p>Those fields usually include things like:</p>
<ul>
<li><p>content type constraints</p>
</li>
<li><p>metadata</p>
</li>
<li><p>the upload policy</p>
</li>
<li><p>the AWS SigV4 credentials</p>
</li>
</ul>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Mutation {
  generateUploadUrl(<span class="hljs-symbol">input:</span> GenerateUploadUrlInput!): UploadUrlResponse!
}

<span class="hljs-keyword">input</span> GenerateUploadUrlInput {
  <span class="hljs-symbol">fileName:</span> String!
  <span class="hljs-symbol">contentType:</span> String!
}

<span class="hljs-keyword">type</span> UploadUrlResponse {
  <span class="hljs-symbol">url:</span> AWSURL!
  <span class="hljs-symbol">fields:</span> AWSJSON!
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You might wonder why we use a <strong>Mutation</strong> to generate a pre-signed URL instead of a Query. That’s because creating a pre-signed URL is an <strong>action with side effects</strong>: i.e. it generates temporary credentials that can be used to perform actions in the system.</div>
</div>

<p>We also need an AWS Lambda resolver. First, it validates that the user is trying to upload a file type that is accepted. Then, it adds a few additional constraints that the client must respect in order to upload the file:</p>
<ul>
<li><p>Min/max file size: e.g. 5 MB</p>
</li>
<li><p>Validates the file type to prevent the user to upload unwanted files and enforces it into the policy</p>
</li>
<li><p>Makes the acl <code>private</code> for the uploaded object</p>
</li>
<li><p>Attaches metadata that tracks the ownership of the file on S3</p>
</li>
</ul>
<p>This is also where you would do any sort of validation like:</p>
<ul>
<li><p>Does the user have the right role/permission to upload a file?</p>
</li>
<li><p>Do they have access to uploads for this resource (project/group/etc.)?</p>
</li>
<li><p>etc.</p>
</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createPresignedPost } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/s3-presigned-post'</span>;
<span class="hljs-keyword">import</span> { S3Client } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/client-s3'</span>;
<span class="hljs-keyword">import</span> { AppSyncResolverEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-lambda'</span>;
<span class="hljs-keyword">import</span> { get } <span class="hljs-keyword">from</span> <span class="hljs-string">'env-var'</span>;

<span class="hljs-keyword">type</span> GenerateUploadUrlInput = {
  input: {
    fileName: <span class="hljs-built_in">string</span>;
    contentType: <span class="hljs-built_in">string</span>;
  };
};

<span class="hljs-keyword">const</span> s3Client = <span class="hljs-keyword">new</span> S3Client({
  region: process.env.AWS_REGION,
});

<span class="hljs-keyword">const</span> BUCKET_NAME = get(<span class="hljs-string">'BUCKET_NAME'</span>).required().asString();

<span class="hljs-keyword">const</span> ALLOWED_CONTENT_TYPES = [<span class="hljs-string">'image/jpeg'</span>, <span class="hljs-string">'image/png'</span>, <span class="hljs-string">'application/pdf'</span>];

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (
  event: AppSyncResolverEvent&lt;GenerateUploadUrlInput&gt;,
) =&gt; {
  <span class="hljs-keyword">const</span> { fileName, contentType } = event.arguments.input;

  <span class="hljs-comment">// Validate allowed content types</span>
  <span class="hljs-keyword">if</span> (!ALLOWED_CONTENT_TYPES.includes(contentType)) {
    <span class="hljs-keyword">return</span> {
      errorMessage: <span class="hljs-string">'Invalid content type'</span>,
      errorType: <span class="hljs-string">'InvalidContentType'</span>,
    };
  }

  <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> Ensure the user is allowed to upload a file</span>

  <span class="hljs-keyword">const</span> userId = <span class="hljs-string">'user123'</span>;

  <span class="hljs-keyword">const</span> presignedPost = <span class="hljs-keyword">await</span> createPresignedPost(s3Client, {
    Bucket: BUCKET_NAME,
    Key: <span class="hljs-string">`uploads/<span class="hljs-subst">${userId}</span>/<span class="hljs-subst">${fileName}</span>`</span>,
    Conditions: [
      <span class="hljs-comment">// Limit file size to 5 MB</span>
      [<span class="hljs-string">'content-length-range'</span>, <span class="hljs-number">0</span>, <span class="hljs-number">5</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>],
      <span class="hljs-comment">// Ensure the uploaded file type matches the onerequested by the user</span>
      [<span class="hljs-string">'eq'</span>, <span class="hljs-string">'$Content-Type'</span>, contentType],
    ],
    Fields: {
      <span class="hljs-comment">// Make the file private</span>
      acl: <span class="hljs-string">'private'</span>,
      <span class="hljs-comment">// Add custom metadata</span>
      [<span class="hljs-string">`x-amz-meta-user-id`</span>]: userId,
    },

    <span class="hljs-comment">// Set expiration time to 5 minutes</span>
    Expires: <span class="hljs-number">60</span> * <span class="hljs-number">5</span>,
  });

  <span class="hljs-keyword">return</span> {
    data: {
      url: presignedPost.url,
      fields: presignedPost.fields,
    },
  };
};
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Remember. The pre-signed url uses the credentials of the signer (In this case, the Lambda function). This means that your Lambda function must have the necessary IAM policy for the <code>s3:PutObject</code> action on the destination bucket.</div>
</div>

<p>The response looks like this</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"generateUploadUrl"</span>: {
      <span class="hljs-attr">"fields"</span>: {
        <span class="hljs-attr">"acl"</span>: <span class="hljs-string">"private"</span>,
        <span class="hljs-attr">"x-amz-meta-user-id"</span>: <span class="hljs-string">"user123"</span>,
        <span class="hljs-attr">"bucket"</span>: <span class="hljs-string">"appsyncs3presignedurlstac-attachmentsbucket8e14a36-hckzhkpulj39"</span>,
        <span class="hljs-attr">"X-Amz-Algorithm"</span>: <span class="hljs-string">"AWS4-HMAC-SHA256"</span>,
        <span class="hljs-attr">"X-Amz-Credential"</span>: <span class="hljs-string">"ASIAWMFUPLSIV2CJPSQ6/20251221/us-east-1/s3/aws4_request"</span>,
        <span class="hljs-attr">"X-Amz-Date"</span>: <span class="hljs-string">"20251221T165443Z"</span>,
        <span class="hljs-attr">"X-Amz-Security-Token"</span>: <span class="hljs-string">"IQoJb3JpZ2luX2VjEBk..."</span>,
        <span class="hljs-attr">"key"</span>: <span class="hljs-string">"uploads/user123/picture.jpg"</span>,
        <span class="hljs-attr">"Policy"</span>: <span class="hljs-string">"eyJleHBpcmF0aW...."</span>,
        <span class="hljs-attr">"X-Amz-Signature"</span>: <span class="hljs-string">"0ba25cc..."</span>
      },
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://appsyncs3presignedurlstac-attachmentsbucket8e14a36-hckzhkpulj39.s3.us-east-1.amazonaws.com/"</span>
    }
  }
}
</code></pre>
<p>The <code>url</code> is your S3 bucket url (The one the client can send the POST request to).</p>
<p>The <code>fields</code> must be passed in the form-data upload request performed by the client.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">ℹ</div>
<div data-node-type="callout-text">The <code>fields</code> property is stringified in the AppSync response. For readability, I have parsed and truncated it in the above code snippet.</div>
</div>

<p>Use the returned info to upload your file from the client:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">uploadFile</span>(<span class="hljs-params">{
  file,
  url,
  fields,
}: {
  file: File;
  url: <span class="hljs-built_in">string</span>;
  fields: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;;
}</span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Uploading file..."</span>);

  <span class="hljs-keyword">const</span> request = <span class="hljs-keyword">new</span> XMLHttpRequest();

  <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();

  <span class="hljs-comment">// Add all pre-signed fields</span>
  <span class="hljs-built_in">Object</span>.entries(fields).forEach(<span class="hljs-function">(<span class="hljs-params">[key, value]</span>) =&gt;</span> {
    formData.append(key, value);
  });
  <span class="hljs-comment">// Add the content-type</span>
  formData.append(<span class="hljs-string">"Content-type"</span>, file.type);
  <span class="hljs-comment">// add the file</span>
  formData.append(<span class="hljs-string">"file"</span>, file);

  request.open(<span class="hljs-string">"POST"</span>, url, <span class="hljs-literal">true</span>);
  request.send(formData);
}
</code></pre>
<p>Once the client uploads the file, it will be visible on the S3 bucket at the indicated location. The metadata will also be attached to the object.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766339944861/c998263e-7012-4cc6-9a9f-8357336d94c9.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-generate-a-pre-signed-download-url">⬇️ Generate a pre-signed download url</h2>
<p>Now that they can upload files, your users might also want to download them. Just like with uploads, you can generate a pre-signed download URL.</p>
<p>To do this, we need a new Mutation that takes the file name (object key) as input and returns the URL to download the file.</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Mutation {
  generateDownloadUrl(<span class="hljs-symbol">fileName:</span> String!): DownloadUrlResponse!
}

<span class="hljs-keyword">type</span> DownloadUrlResponse {
  <span class="hljs-symbol">url:</span> AWSURL!
}
</code></pre>
<p>Here too, a Lambda resolver generates the signed url. Optionally, this is also where you can perform authorization checks.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { S3Client, GetObjectCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/client-s3'</span>;
<span class="hljs-keyword">import</span> { AppSyncResolverEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-lambda'</span>;
<span class="hljs-keyword">import</span> { get } <span class="hljs-keyword">from</span> <span class="hljs-string">'env-var'</span>;
<span class="hljs-keyword">import</span> { getSignedUrl } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/s3-request-presigner'</span>;

<span class="hljs-keyword">type</span> GenerateDownloadUrlInput = {
  fileName: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">const</span> s3Client = <span class="hljs-keyword">new</span> S3Client({
  region: process.env.AWS_REGION,
});

<span class="hljs-keyword">const</span> BUCKET_NAME = get(<span class="hljs-string">'BUCKET_NAME'</span>).required().asString();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (
  event: AppSyncResolverEvent&lt;GenerateDownloadUrlInput&gt;,
) =&gt; {
  <span class="hljs-keyword">const</span> { fileName } = event.arguments;

  <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> GetObjectCommand({
    Bucket: BUCKET_NAME,
    Key: fileName,
  });

  <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">await</span> getSignedUrl(s3Client, command, {
    expiresIn: <span class="hljs-number">60</span> * <span class="hljs-number">5</span>,
  });

  <span class="hljs-keyword">return</span> {
    data: {
      url: url,
    },
  };
};
</code></pre>
<p>Response example</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"generateDownloadUrl"</span>: {
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://appsyncs3presignedurlstac-attachmentsbucket8e14a36-hckzhkpulj39.s3.us-east-1.amazonaws.com/uploads/user123/picture.jpg?X-Amz-Algorithm=..."</span>
    }
  }
}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By using <strong>pre-signed S3 URLs</strong>, you keep file transfers out of your GraphQL API while still maintaining full control over <strong>who can upload or download what, and under which conditions</strong>. AppSync becomes the gatekeeper — validating intent, enforcing security rules, and issuing short-lived permissions — while Amazon S3 handles the heavy lifting with speed and scale.</p>
<p>This approach gives you:</p>
<ul>
<li><p>Secure, direct uploads and downloads</p>
</li>
<li><p>Clear separation between control plane and data plane</p>
</li>
<li><p>Fine-grained control over authorization, file size, MIME types, and metadata</p>
</li>
</ul>
<p>You can find the code of this project on <a target="_blank" href="https://github.com/bboure/appsync-upload-to-s3"><strong>Github</strong></a><strong>.</strong></p>
<p>Thanks for reading, and happy building! 🚀</p>
]]></content:encoded></item><item><title><![CDATA[AWS AppSync Best Practices]]></title><description><![CDATA[AWS AppSync is a serverless offering from AWS that allows you to create scalable and efficient GraphQL APIs. It is fully managed, meaning that you don't need to worry about managing infrastructure or servers.
Like any other service, there are some be...]]></description><link>https://blog.graphbolt.dev/aws-appsync-best-practices</link><guid isPermaLink="true">https://blog.graphbolt.dev/aws-appsync-best-practices</guid><category><![CDATA[aws appsync]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[AWS Best Practices]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Thu, 29 Feb 2024 07:33:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708961195082/e3dd007a-cd7a-4370-a88c-bcdaebfd00d1.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS AppSync is a serverless offering from AWS that allows you to create scalable and efficient GraphQL APIs. It is fully managed, meaning that you don't need to worry about managing infrastructure or servers.</p>
<p>Like any other service, there are some best practices that you should follow to make sure that your APIs remain secure, maintainable, and scaleable. In this blog post, I'd like to share the best practices I learned after several years of practice.</p>
<h2 id="heading-avoid-lambda-resolvers">Avoid Lambda Resolvers</h2>
<p>AWS AppSync integrates directly with 6 data sources outside Lambda: <em>DynamoDB</em>, <em>OpenSearch</em>, <em>EventBridge</em>, <em>HTTP</em>, <em>Amazon RDS</em>, and <em>None</em> (a special data source that does not connect to any store). If you only need to interact with any of those sources, use them. The benefits are the following:</p>
<ul>
<li><p><strong>Lower latency</strong>: By interacting directly with the data source, you remove one "hop" in the data flow, and you also avoid Lambda cold starts. Your API will feel faster and more responsive to the end user.</p>
</li>
<li><p><strong>Lower cost</strong>: Even if your Lambda functions are warm, you are still billed for invocation and runtime. By removing Lambda, you will only be billed for the invoked underlying data store (e.g. DynamoDB).</p>
</li>
<li><p><strong>Maintenance &amp; Monitoring</strong>: Removing a Lambda function will also lower the burden and cost of maintenance and observability. It's one thing less to monitor in your system.</p>
</li>
</ul>
<p>In the early days, the dreaded VTL language has often been a reason - ahem, an excuse <strong>-</strong> to use Lambda functions. Today, with support for <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/tutorials-js.html">JS resolvers</a> and its <a target="_blank" href="https://www.npmjs.com/package/@aws-appsync/utils">utility package</a>, it has become easier than ever to write blazing-fast resolvers. And if you prefer, it is even possible to <a target="_blank" href="https://blog.graphbolt.dev/improving-developer-experience-with-typescript-how-to-write-strongly-typed-appsync-resolvers">write AppSync resolvers in TypeScript</a>.</p>
<h2 id="heading-dont-use-direct-lambda-resolver">Don't use Direct Lambda Resolver</h2>
<p>I just explained why you should avoid Lambda resolvers, but sometimes you have no other choice. You might need to run complex business logic, invoke unsupported data sources (<a target="_blank" href="https://blog.iamjkahn.com/2019/12/invoking-even-more-aws-services-directly-from-aws-appsync.html">are you sure about that</a>?), you are limited by the <a target="_blank" href="https://blog.graphbolt.dev/everything-you-should-know-about-the-appsync-javascript-pipeline-resolvers">APPSYNC_JS runtime</a>, or just prefer writing resolvers in your favorite language. If you do so, using the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/direct-lambda-reference.html">direct Lambda integration</a> might be tempting. Don't.</p>
<p>The biggest problem with the direct Lambda integration is that it moves the handling of the GraphQL response back to the Lambda function. This is especially a problem for error handling because it means that if you want to return an error to the user, you must throw an error from the lambda function code. This raises several issues:</p>
<ul>
<li><strong>Less control over the error returned in the request</strong></li>
</ul>
<p>AWS AppSync allows you to return detailed errors using the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/built-in-util-js.html#utility-helpers-in-error-js">util.error</a> helper function. This gives you more flexibility on what you want to return to the user, and how. A thrown error offers less flexibility. Worst, it might leak information about your implementation to the outside world. You also can't control which error to return. For example, AppSync has a special <code>util.unauthorized()</code> error util that you can use to notify the user about unauthorized access.</p>
<ul>
<li><strong>It messes with Observability</strong></li>
</ul>
<p>Do you want to be woken up in the middle of the night because a user sent an invalid request? Throwing errors from the Lambda function will count as a failed execution. It will show up as such in your metrics and might trigger alarms unnecessarily. Controlled errors (i.e. <code>Unauthorized</code>, <code>NotFound</code>, <code>ValidationError</code>, etc) should not cause the Lambda function to fail. Only code errors (a.k.a. bugs) should.</p>
<p><strong>What to do instead?</strong></p>
<p>Instead of throwing errors from the Lambda function, return them as part of the response payload.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-comment">//...</span>

  <span class="hljs-keyword">if</span> (!isAuthorized(user)) {
    <span class="hljs-comment">// ❌ DON'T</span>
    <span class="hljs-comment">// throw new Error('Unauthorized');</span>

    <span class="hljs-comment">// ✅ DO</span>
    <span class="hljs-keyword">return</span> {
      error: {
        name: <span class="hljs-string">'Unauthorized'</span>,
        message: <span class="hljs-string">'Not allowed'</span>,
      }
    }
  }

  <span class="hljs-keyword">if</span> (!data) {
    <span class="hljs-comment">// ❌ DON'T</span>
    <span class="hljs-comment">// throw new Error('NotFound');</span>

    <span class="hljs-comment">// ✅ DO</span>
    <span class="hljs-keyword">return</span> {
      error: {
        name: <span class="hljs-string">'NotFound'</span>,
        message: <span class="hljs-string">'Resource Not Found'</span>,
      }
    }
  }

  <span class="hljs-keyword">return</span> { data };
}
</code></pre>
<p>You can then do simple checks in your response handler to see if any error was returned and handle it there.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> response (ctx) =&gt; {
    <span class="hljs-keyword">const</span> { data, error } = ctx.result;
    <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-keyword">const</span> { name, message } = error;
        <span class="hljs-keyword">if</span> (name === <span class="hljs-string">'Unauthorized'</span>) {
            util.unauthorized();
        } <span class="hljs-keyword">else</span> {
            util.error(name, message);
        }
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> data;
    }
}
</code></pre>
<h2 id="heading-always-use-pipepeline-resolvers">Always Use Pipepeline Resolvers</h2>
<p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/pipeline-resolvers-js.html">Pipeline resolvers</a> allow you to execute several operations and/or connect to several data sources to resolve a single GraphQL field. For example, you might need to retrieve two DyamoDB items and merge them, or add an authorization layer in a multi-tenant application.</p>
<p>Even if a field only requires one operation, I tend to always default pipeline resolvers. I just create a one-function pipeline. Why? Because if I ever need to add an operation, I can just add a function in the pipeline without refactoring everything. Easy.</p>
<p>Another reason is that pipeline functions are re-useable (They can be in more than one resolver). Which brings me to my next point.</p>
<h2 id="heading-create-re-useable-resolver-handlers">Create Re-useable Resolver Handlers</h2>
<p>In a GraphQL API, some resolvers will often look similar. For example, resolvers for <code>Query.getUser(id: ID!)</code> and <code>Order.user</code> both resolve to a user. The only difference might just be where the <code>id</code> argument comes from (e.g. <code>ctx.arguments.id</code> and <code>ctx.source.userId</code> respectively). The rest is identical: both get a user from the data source with an id. This means that you can use the same resolver code, or pipeline function for both use cases. You don't have to duplicate them.</p>
<p>One neat trick is to use the context object to pass the data that you need to the handler (e.g. using <code>ctx.prev</code> or <code>ctx.stash</code>).</p>
<p>Example:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// get user request handler</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">return</span> {
    operation: <span class="hljs-string">'GetItem'</span>,
    key: util.dynamodb.toMapValues({ id: ctx.prev.userId }),
  };
}
</code></pre>
<p>Using pipeline resolvers makes it even easier. You can use the <em>before</em> handler to pass the value.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Query.user "before" handler</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">return</span> {
    userId: ctx.arguments.id,
  };
}
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// Order.user "before" handler</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">return</span> {
    userId: ctx.source.userId,
  };
}
</code></pre>
<p>This will reduce the amount of code and maintenance. Be careful though, if the requirements of one resolver change, but not the other, you might need to separate them again.</p>
<h2 id="heading-use-resolver-batching">Use Resolver Batching</h2>
<p>OK, if there is one good reason to use a Lambda resolver after all, it would be batching. By default, AppSync will run all the resolvers of the same level of nesting in parallel (up to a certain limit), and as far as I could observe, it also tries to duplicate identical queries (e.g. DynamoDB GetItem with the same key). But for even better efficiency and control over how nested resolvers are executed, you can use AppSync's <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-lambda-resolvers.html#advanced-use-case-batching">batching feature</a> on Lambda resolvers.</p>
<p>It allows you to receive all the nested resolver events in a single Lambda function invocation (this is configurable). My fellow Community Builder <a target="_blank" href="https://twitter.com/richdevelops?lang=en">Rich Buggy</a> wrote <a target="_blank" href="https://www.richdevelops.dev/solve-the-appsync-lambda-resolver-n-plus-1-problem-with-batchinvoke">a great article</a> about this. I recommend you give it a read.</p>
<h2 id="heading-dont-keep-secrets-in-resolvers-code">Don't Keep Secrets in Resolvers Code</h2>
<p>If you are using a resolver to access external resources (e.g. a third-party API) that require an API key, or secret, you should not hardcode them in the resolver's code. You should also not keep them in <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/environmental-variables.html">environment variables</a>, as explained in the documentation. Instead, use <a target="_blank" href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html">Secret Manager</a>, or the <a target="_blank" href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html">Systems Manager Parameter Store</a> to retrieve them. You can achieve that with a Pipeline resolver that fetches the secret first, followed by the external request.</p>
<p>One small drawback is that the value will need to be fetched for every single request. Unlike in a Lambda function, you cannot store the secret outside the handler and reuse it for further invocations. This can increase both latency and cost.</p>
<h2 id="heading-use-caching">Use Caching</h2>
<p>AppSync allows you to add a caching layer in front of your API. You can either choose to cache full requests or granularly decide which resolvers use cache or not. You can even control what the caching key is for each one of them.</p>
<p>Although caching adds an extra fixed cost (based on the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/enabling-caching.html#caching-instances">instance type</a>), it can actually save you other costs and even result in being cheaper compared to hitting the same data source over and over again.</p>
<p>Of course, needless to say, enabling cache will also boost the performance of your API for requests that have been previously saved, reducing latency.</p>
<h2 id="heading-choose-the-right-authorization-method">Choose the Right Authorization Method</h2>
<p>AWS AppSync supports 5 different authorizers. To ensure the security of your API, it's important to use the correct one for every use case.</p>
<ul>
<li>Cognito User Pools</li>
</ul>
<p>Cognito User Pool is a fully managed AWS service that serves as a user directory. Use it for user-facing APIs which require identifying which user is executing the request. With Cognito User Pools, you can even have <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#using-additional-authorization-modes">advanced control</a> over who can access some mutations, queries, or subscriptions through the <code>@aws_cognito_user_pools</code> directive. This authorization filter will happen before the resolver is even called.</p>
<ul>
<li>API Key</li>
</ul>
<p>This authorizer will provide you with one or more API keys that you use to invoke the API. This authorizer is a simple way to get started with AWS AppSync, and create quick proof of concepts or demos.</p>
<p>Another common use case for API keys is for "public" APIs. By default, AppSync does not allow any request to be unauthenticated. To go around that, you can create an API key and use it in your front end, but keep in mind that the API key will be publicly visible. For more secure guest/anonymous requests, you can use <a target="_blank" href="https://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html">Cognito Identity Pools</a> in conjunction with an IAM authorizer.</p>
<ul>
<li>IAM</li>
</ul>
<p>The IAM authorizer requires every request to be signed with IAM SigV4. Use it to invoke queries or mutations from known and trusted actors with an IAM role, such as your back end. This is also the authorizer required for the <a target="_blank" href="https://blog.graphbolt.dev/building-an-event-driven-application-with-aws-appsync-eventbridge-and-step-functions">EventBridge-AppSync integration</a>.</p>
<ul>
<li>OpenId Connect (OIDC)</li>
</ul>
<p>OIDC can be used to integrate with third-party authorizers. e.g. Okta, or Auth0. It requires a valid JWT created by a vendor. AppSync will validate the token after it receives it before allowing, or denying the request.</p>
<p>Use it for existing user bases outside AWS, or if you need/want to use an external provider.</p>
<ul>
<li>Lambda Authorizer</li>
</ul>
<p>This method gives you the most flexibility, but it also puts all the responsibility of security on you. Only use it if you know what you are doing, or have a specific advanced use case.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">AWS AppSync supports several authorizers on the same API. You can combine them and control what queries and fields each one can access. This allows you to tailor the best security possible.</div>
</div>

<h2 id="heading-protect-your-api">Protect your API</h2>
<p>After you deploy an API, you want to protect it against bad actors.</p>
<p>I just talked about it: one of the most important aspects of security is to only allow access to users/services that you trust by using the right authorizers. But you don't have to limit it to that. You also need to protect it against possible attacks.</p>
<p>There are several things you can do to prevent that.</p>
<ul>
<li><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/configuration-limits.html#configuration-limits-depth">Set query depth limits</a></li>
</ul>
<p>The nature of GraphQL allows you to nest types and fields together. This allows the client to retrieve all the data it needs in a single request. By default, there is no limit to how much you can nest queries, and this could be used as an exploit by attackers to overload your system.</p>
<p>Imagine the following query:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">query</span> user(<span class="hljs-symbol">id:</span> <span class="hljs-string">"123"</span>) {
    name
    friends {
        name
        friends {
            name
            friends {
                name
                friends { <span class="hljs-punctuation">... </span>}
            }
        }
    }
}
</code></pre>
<p>I could keep going with it as much as I'd like. Although <a target="_blank" href="https://blog.graphbolt.dev/the-aws-appsync-limits-you-need-to-know">AppSync has some limits</a> in place (e.g. 30 seconds execution time), such queries can increase the complexity of requests, which increases the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html#aws-appsync-using-token-counts-to-optimize-requests">consumption of tokens</a>. It will also likely incur additional costs (e.g. more DynamoDB requests).</p>
<p>To protect you against that, you can configure how deep queries can go. Any request that goes over that limit will be blocked.</p>
<ul>
<li>Add a Web Application Firewall (WAF)</li>
</ul>
<p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/WAF-Integration.html">AWS WAF</a> is a fully managed service designed to protect APIs against common exploits. It is integrated with AWS AppSync and can be used as an extra layer of security.</p>
<p>With AWS WAF, you can for example block or allow access to certain IP addresses, rate limit callers, block access from certain countries, etc. AWS WAF acts as an extra layer on top of AppSync, meaning that only allowed requests will hit your API.</p>
<ul>
<li>Use private APIs</li>
</ul>
<p>AppSync has support for <a target="_blank" href="https://aws.amazon.com/blogs/mobile/introducing-private-apis-on-aws-appsync/">private APIs</a>. If your API is used for internal purposes only, you can deploy it in a private network. Only systems/clients that are in the same VPC will be able to access it.</p>
<ul>
<li>Disable Introspection</li>
</ul>
<p>One of the great benefits of GraphQL is that it's self-documented. There is a special request that you can make to any API, called an <a target="_blank" href="https://graphql.org/learn/introspection/">introspection</a> query. This allows the caller to explore the schema of the API and all its types and fields. However, there are cases where you might want to disallow public users from introspecting the schema. For example, you might want them to avoid discovering hidden, or private queries and mutations.</p>
<p>If this is your case, AppSync allows you to <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/configuration-limits.html">disable introspection</a> through the API settings.</p>
<h2 id="heading-disable-verbose-logging-on-production">Disable Verbose Logging on Production</h2>
<p>AppSync CloudWatch logs can be very useful; especially to <a target="_blank" href="https://blog.graphbolt.dev/debugging-aws-appsync-apis-with-cloudwatch">debug AWS AppSync</a> requests. But it can also be very expensive when enabled on production.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/Benoit_Boure/status/1534889345286656000">https://twitter.com/Benoit_Boure/status/1534889345286656000</a></div>
<p> </p>
<p>To avoid excessive costs, you can keep the log level to <code>ERROR</code> to ensure that only errors are logged for further investigation.</p>
<p>Alternatively, you can use log sampling. This is not supported natively by AWS AppSync, but <a target="_blank" href="https://twitter.com/theburningmonk">Yan Cui</a> explains how you can achieve it in <a target="_blank" href="https://theburningmonk.com/2020/09/how-to-sample-appsync-resolver-logs/">this blog post</a>.</p>
<h2 id="heading-use-infrastructure-as-code">Use Infrastructure as Code</h2>
<p>Last, but not least, use <a target="_blank" href="https://en.wikipedia.org/wiki/Infrastructure_as_code">infrastructure as code</a> (IaC). This one might seem obvious but many tutorials, demos, etc. that you can find online often rely on the AWS console. In a real-world application, you want to keep your API's code versioned and make it reproducible and re-deployable in various environments. The choice of IaC is up to you though. It could be the <a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/home.html">CDK</a>, the <a target="_blank" href="https://www.serverless.com/">Serverless Framework</a>, or <a target="_blank" href="https://aws.amazon.com/serverless/sam/">SAM</a>, for example. Just use IaC!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AWS AppSync is an amazing service for writing efficient, scaleable GraphQL API with serverless technology. Following the best practices will only make it better.</p>
<p>To improve this experience even more, I created <a target="_blank" href="https://graphbolt.dev?utm_campaign=appsync-best-practices&amp;utm_medium=Blog&amp;utm_source=blog.graphbolt.dev">GraphBolt</a>, a desktop application to help you test and debug AppSync APIs, and follow those best practices. <a target="_blank" href="https://graphbolt.dev?utm_campaign=appsync-best-practices&amp;utm_medium=Blog&amp;utm_source=blog.graphbolt.dev">Try it today for free</a>!</p>
<p>If you are new to AppSync, I also created a <a target="_blank" href="https://appsync.wtf/workshops/typescript-workshop">free workshop</a> where you can get started.</p>
<p>Thanks for reading!</p>
<p>If you like this kind of content, feel free to follow me on <a target="_blank" href="https://hashnode.com/@bboure">Hashnode</a>, <a target="_blank" href="https://twitter.com/Benoit_Boure">X</a>, and <a target="_blank" href="https://www.linkedin.com/in/bboure/">LinkedIn</a>. You can also subscribe to this blog's newsletter to receive notifications about new posts.</p>
]]></content:encoded></item><item><title><![CDATA[Building an Event-Driven Application with AWS AppSync, EventBridge, and Step Functions]]></title><description><![CDATA[Over the past years, event-driven architecture has become more and more popular. With the arrival of services such as EventBridge and its integration with many other AWS services, building such applications has become easier than ever.
Last year, AWS...]]></description><link>https://blog.graphbolt.dev/building-an-event-driven-application-with-aws-appsync-eventbridge-and-step-functions</link><guid isPermaLink="true">https://blog.graphbolt.dev/building-an-event-driven-application-with-aws-appsync-eventbridge-and-step-functions</guid><category><![CDATA[AppSync]]></category><category><![CDATA[aws appsync]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[event-driven]]></category><category><![CDATA[stepfunction]]></category><category><![CDATA[AWS Step Functions]]></category><category><![CDATA[AWS EventBridge]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[CDK]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Thu, 22 Feb 2024 17:27:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708622121967/e2a2b1c0-a5fb-4984-8590-285dfc9bc5fb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over the past years, event-driven architecture has become more and more popular. With the arrival of services such as EventBridge and its integration with many other AWS services, building such applications has become easier than ever.</p>
<p>Last year, <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2023/04/aws-appsync-publishing-events-amazon-eventbridge/">AWS announced support for Amazon EventBridge as a Data Source for AWS AppSync</a>, and more recently, it was the turn of <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2024/01/amazon-eventbridge-appsync-target-buses/">EventBridge to integrate with AppSync</a>. The two-way integration between those two services opens many opportunities.</p>
<p>In this article, I will show you how you can create an event-driven system with real-time updates using AWS AppSync, EventBridge, and Step Functions.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">To maintain brevity, this article assumes a certain level of familiarity with those services.</div>
</div>

<h2 id="heading-what-we-will-build">What We Will Build</h2>
<p>We will build a simple food-ordering service where users can place an order for their meal. The order will be received by the back-end through an AWS AppSync GraphQL API. The Orders service will process it and send real-time updates to the user about the different status updates (e.g. Paid, Preparing, Out for delivery, Delivered, etc.). The Orders service is a Step Functions workflow that orchestrates the different steps of the process, while Amazon EventBridge asynchronously coordinates all the services.</p>
<p>Here is a high-level diagram of what this looks like.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708065692079/0d25c9c7-bf1a-4bf9-b8b2-1720d11ad17c.png" alt class="image--center mx-auto" /></p>
<p>To build this, we will use the CDK as our Infrastructure as Code (IaC).</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">👨‍💻</div>
<div data-node-type="callout-text">Just want to see the code? You can find the <a target="_blank" href="https://github.com/bboure/serverless-eda-graphql-api">final solution on GitHub</a>.</div>
</div>

<h2 id="heading-the-appsync-api">The AppSync API</h2>
<p>First, we'll need an AppSync API. It will be the entry point for our users to place orders. Here is the simplified schema.</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Product {
  <span class="hljs-symbol">name:</span> String!
  <span class="hljs-symbol">quantity:</span> Int!
}

<span class="hljs-keyword">type</span> Order {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">status:</span> OrderStatus!
  <span class="hljs-symbol">products:</span> [Product]!
  <span class="hljs-symbol">createdAt:</span> AWSDateTime!
  <span class="hljs-symbol">updatedAt:</span> AWSDateTime!
}

<span class="hljs-keyword">input</span> ProductInput {
  <span class="hljs-symbol">name:</span> String!
  <span class="hljs-symbol">quantity:</span> Int!
}

<span class="hljs-keyword">input</span> CreateOrderInput {
  <span class="hljs-symbol">products:</span> [ProductInput!]!
}

<span class="hljs-keyword">input</span> NotifyOrderUpdatedInput {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">status:</span> OrderStatus!
  <span class="hljs-symbol">updatedAt:</span> AWSDateTime!
}

<span class="hljs-keyword">type</span> OrderUpdated <span class="hljs-meta">@aws_api_key</span> <span class="hljs-meta">@aws_iam</span> {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">status:</span> OrderStatus!
  <span class="hljs-symbol">updatedAt:</span> AWSDateTime!
}

<span class="hljs-keyword">enum</span> OrderStatus {
  PENDING
  PAID
  PREPARING
  OUT_FOR_DELIVERY
  DELIVERED
  CANCELLED
}

<span class="hljs-keyword">type</span> Mutation {
  createOrder(<span class="hljs-symbol">input:</span> CreateOrderInput!): Order
  notifyOrderUpdated(<span class="hljs-symbol">input:</span> NotifyOrderUpdatedInput!): OrderUpdated! <span class="hljs-meta">@aws_iam</span>
}

<span class="hljs-keyword">type</span> Subscription {
  onOrderUpdated(<span class="hljs-symbol">id:</span> ID!): OrderUpdated
    <span class="hljs-meta">@aws_subscribe</span>(<span class="hljs-symbol">mutations:</span> [<span class="hljs-string">"notifyOrderUpdated"</span>])
}
</code></pre>
<p>The schema contains 2 mutations:</p>
<ul>
<li><code>createOrder</code>: can be used by the front end to place an order</li>
</ul>
<p>This mutation is attached to a <a target="_blank" href="https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/appsync-construct.ts#L55-L80">pipeline resolver</a> with two functions: <code>createOrder</code> and <code>putEvent</code>.</p>
<p><code>createOrder</code> uses a DynamoDB data source to persist the order in a table, and prepares an <code>order.created</code> event containing its details before storing it in the stash.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// createOrder resolver</span>
<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">import</span> { put } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-appsync/utils/dynamodb'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> request = <span class="hljs-function">(<span class="hljs-params">ctx</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> order = {
    id: util.autoId(),
    status: <span class="hljs-string">'PENDING'</span>,
    products: ctx.arguments.input.products,
    createdAt: util.time.nowISO8601(),
    updatedAt: util.time.nowISO8601(),
  };

  <span class="hljs-keyword">return</span> put({
    key: {
      id: order.id,
    },
    item: order,
  });
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> response = <span class="hljs-function">(<span class="hljs-params">ctx</span>) =&gt;</span> {
  <span class="hljs-comment">// Here, we prepare the `order.created` event for the next step</span>
  ctx.stash.event = { detailType: <span class="hljs-string">'order.created'</span>, detail: ctx.result };

  <span class="hljs-keyword">return</span> ctx.result;
};
</code></pre>
<p><code>putEvent</code> is attached to an EventBridge data source. It receives an event (from the stash) and puts it into the event bus. We'll see later how it is used.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// putEvent resolver</span>
<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">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> { event } = ctx.stash;

  <span class="hljs-keyword">if</span> (!event) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'No event found in stash'</span>);
    util.error(<span class="hljs-string">'InternalError'</span>);
  }

  <span class="hljs-keyword">return</span> {
    operation: <span class="hljs-string">'PutEvents'</span>,
    events: [
      {
        source: <span class="hljs-string">'order.api'</span>,
        ...event,
      },
    ],
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span>(<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">return</span> ctx.prev.result;
}
</code></pre>
<ul>
<li><code>notifyOrderUpdate</code> will be used by the back end to send real-time updates.</li>
</ul>
<p>It is attached to a <a target="_blank" href="https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/appsync-construct.ts#L82-L88">resolver with a <em>None</em> data source</a> because this mutation does not persist anything in any store. Its only purpose is to trigger a notification to the <code>onOrderUpdated</code> subscription.</p>
<p>The API also uses <a target="_blank" href="https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/appsync-construct.ts#L41-L47">two authorizers</a>: <code>API_KEY</code> (for users to place orders) and <code>IAM</code> for the back end to call the <code>notifyOrderUpdated</code> mutation (this is a requirement from the <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/target-appsync.html">EventBridge integration</a>).</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In a real app, we'd probably use a Cognito User Pool, or OIDC, for user-facing authentication. I used an API key for simplicity.</div>
</div>

<h2 id="heading-the-step-functions-state-machine">The Step Functions State Machine</h2>
<p>The second component of our application is a <a target="_blank" href="https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/state-machine-construct.ts">Step Functions state machine</a>. This state machine orchestrates the different steps of processing an order: payment, waiting for the restaurant to prepare the meal, waiting for delivery, etc.</p>
<p>For simplicity, I used <a target="_blank" href="https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-wait-state.html">wait tasks</a> to simulate the workflow. In a real application, the state machine would wait for real processes (e.g. payment gateway), or humans (e.g. meal preparation) before transitioning between the different states (hint: probably using the <a target="_blank" href="https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token">callback pattern</a>).</p>
<p>The most important part is the <code>EventBridgePutEvents</code> tasks. After every step in the order processing workflow, it receives the updated order and puts an event into the event bus with all its details. In the next section, we'll see how they are being handled.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/state-machine-construct.ts</span>

createNotifyUpdate(name: <span class="hljs-built_in">string</span>) {
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> EventBridgePutEvents(<span class="hljs-built_in">this</span>, <span class="hljs-string">`Notify <span class="hljs-subst">${name}</span>`</span>, {
    entries: [
      <span class="hljs-comment">// generates an event from the task input</span>
      {
        source: <span class="hljs-string">'order.processing'</span>,
        detailType: <span class="hljs-string">'order.updated'</span>,
        detail: TaskInput.fromObject({
          <span class="hljs-string">'order.$'</span>: <span class="hljs-string">'$.order'</span>,
        }),
        eventBus: <span class="hljs-built_in">this</span>.eventBus,
      },
    ],
    resultPath: <span class="hljs-string">'$.eventBridgeResult'</span>,
  });
}
</code></pre>
<h2 id="heading-put-it-all-together">Put It All Together</h2>
<p>So far, we've learned about the AppSync API, and the Step Functions workflow. Did you notice that both those services place events into EventBridge? What I haven't shown you yet is how those events are used and how they play together.</p>
<p>In our application, the first thing that happens is a user placing an order. That's the starting point of the whole process, and it results in an <code>order.created</code> event. If you look at our <a target="_blank" href="https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/state-machine-construct.ts">state machine construct</a>, you'll notice that it subscribes to those events to start a new execution.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">new</span> Rule(<span class="hljs-built_in">this</span>, <span class="hljs-string">'OrderHandlerRule'</span>, {
  eventBus: eventBus,
  eventPattern: {
    <span class="hljs-comment">// subscribe to order.created events, coming from the api.</span>
    source: [<span class="hljs-string">'order.api'</span>],
    detailType: [<span class="hljs-string">'order.created'</span>],
  },
  targets: [
    <span class="hljs-keyword">new</span> SfnStateMachine(sm, {
      input: RuleTargetInput.fromObject({
        order: EventField.fromPath(<span class="hljs-string">'$.detail'</span>),
      }),
    }),
  ],
});
</code></pre>
<p>This means, that each time an order is placed, a Step Functions workflow will start with the order details as its input.</p>
<p>Then, as the state machine goes over all the different steps of the order processing, it emits <code>order.updated</code> events, which in turn are picked up by an EventBridge-AppSync rule.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">new</span> CfnRule(scope, <span class="hljs-string">'UpdateOrder'</span>, {
  eventBusName: eventBus.eventBusName,
  eventPattern: {
    source: [<span class="hljs-string">'order.processing'</span>],
    <span class="hljs-string">'detail-type'</span>: [<span class="hljs-string">'order.updated'</span>],
  },
  targets: [
    {
      id: <span class="hljs-string">'OrderUpdated'</span>,
      arn: (api.node.defaultChild <span class="hljs-keyword">as</span> CfnGraphQLApi).attrGraphQlEndpointArn,
      roleArn: ebRuleRole.roleArn,
      appSyncParameters: {
        graphQlOperation: <span class="hljs-string">`mutation NotifyOrderUpdated($input: NotifyOrderUpdatedInput!) { notifyOrderUpdated(input: $input) { id status updatedAt } }`</span>,
      },
      inputTransformer: {
        inputPathsMap: {
          id: <span class="hljs-string">'$.detail.order.id'</span>,
          status: <span class="hljs-string">'$.detail.order.status'</span>,
          updatedAt: <span class="hljs-string">'$.detail.order.updatedAt'</span>,
        },
        inputTemplate: <span class="hljs-built_in">JSON</span>.stringify({
          input: {
            id: <span class="hljs-string">'&lt;id&gt;'</span>,
            status: <span class="hljs-string">'&lt;status&gt;'</span>,
            updatedAt: <span class="hljs-string">'&lt;updatedAt&gt;'</span>,
          },
        }),
      },
    },
  ],
});
</code></pre>
<p>This is a direct integration between EventBridge and AWS AppSync. EventBridge invokes the <code>notifyOrderUpdated</code> mutation and uses the event's <code>details</code> attributes to build the input variables of the request. In turn, the mutation emits a message to all subscribers of the <code>onOrderUpdated</code> subscription.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">At the time of writing this article, there is no L2 construct for the EventBridge-AppSync integration, but we can use the L1 <code>CfnRule</code> construct.</div>
</div>

<p>With all this in place, the front end can use the <code>onOrderUpdate</code> subscription to get real-time updates as soon as it places an order.</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">subscription</span> OnOrderUpdated(<span class="hljs-variable">$id</span>: ID!) {
  onOrderUpdated(<span class="hljs-symbol">id:</span> <span class="hljs-variable">$id</span>) {
    id
    status
    updatedAt
  }
}
</code></pre>
<p>Here is a simulation using GraphBolt. Look at the subscription messages coming in. Pay attention to the <code>status</code> field.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708501001608/1aea077d-a87a-4944-ab66-06bb8d43b9d7.gif" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><a target="_blank" href="https://graphbolt.dev?utm_campaign=eda-post&amp;utm_medium=Blog&amp;utm_source=blog.graphbolt.dev&amp;utm_term=graphbolt">GraphBolt</a> is a desktop app to build, test, debug, and manage AWS AppSync APIs. <a target="_blank" href="https://graphbolt.dev?utm_campaign=eda-post&amp;utm_medium=Blog&amp;utm_source=blog.graphbolt.dev&amp;utm_term=try-it-for-free">Try it for free</a> today!</div>
</div>

<h1 id="heading-conclusion">Conclusion</h1>
<p>Leveraging event-driven architecture with AWS AppSync, Step Functions, and EventBridge offers a seamless solution for building efficient, asynchronous, and real-time applications. In this article, I explained with a practical use case how you can achieve it, and the best part is that we almost did not write any code for it (outside the infrastructure).</p>
<p>If you liked this content, please share! You can also follow me on <a target="_blank" href="https://twitter.com/Benoit_Boure">X</a>, <a target="_blank" href="https://hashnode.com/@bboure">Hashnode</a>, and <a target="_blank" href="https://www.linkedin.com/in/bboure/">LinkedIn</a> for more, or subscribe to this blog's <a target="_blank" href="https://blog.graphbolt.dev/newsletter">newsletter</a>.</p>
<p>Thank you!</p>
]]></content:encoded></item><item><title><![CDATA[GraphBolt v1.0.0 is Here 🚀]]></title><description><![CDATA[After opening sales in December, and leaving the Beta phase in January, today, I'm proudly announcing one of the biggest releases to date. This version marks a huge milestone in the project and offers some major features I've wanted to include for a ...]]></description><link>https://blog.graphbolt.dev/graphbolt-v1-is-here</link><guid isPermaLink="true">https://blog.graphbolt.dev/graphbolt-v1-is-here</guid><category><![CDATA[AppSync]]></category><category><![CDATA[aws appsync]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[AWS AppSync Introduction]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Mon, 05 Feb 2024 18:04:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1707152455292/f65f6b9f-e274-4a04-aa58-f66e772470f6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After <a target="_blank" href="https://twitter.com/Benoit_Boure/status/1737912262512320634">opening sales in December</a>, and leaving the Beta phase in January, today, I'm proudly announcing one of the biggest releases to date. This version marks a huge milestone in the project and offers some major features I've wanted to include for a long time. It's so important that it deserved a MAJOR version bump: <strong>v1.0.0</strong> 🚀</p>
<p>In this blog post, I will explain them and tell you how to make the best out of them.</p>
<h1 id="heading-say-hi-to-projects">Say "Hi" to Projects 👋</h1>
<p>When you work on a project, you often need to work on the same service in its different stages (also known as Environments). This is especially true for serverless applications where you often have 3 or more of them. i.e. You probably have <code>dev</code>, <code>staging</code>, <code>prod</code>; plus, every dev might have their own account/environment, without mentioning a bunch of ephemeral (or ci/cd) stacks. That's a lot of environments all representing the same application or service(s)!</p>
<p>More concretely, if you're deploying the same AppSync API to those different environments, you probably want to be able to manage (and test) them in one convenient place.</p>
<p>In this new version of GraphBolt, you can now create <em>Projects</em> to logically group all your API instances, <strong>even if they live in different AWS accounts and regions</strong>.</p>
<p>After selecting AWS profiles and regions, GraphBolt will automatically explore your accounts and discover your AppSync APIs. You can then choose which ones to include in the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707160595608/e8951e94-99d4-4974-93f8-92c6e7be368c.gif" alt class="image--center mx-auto" /></p>
<p>After creating a Project, you can access all your favorite GraphBolt tools, within a single and homogenized scope. This means that, for example, Operation and Resolver collections now belong to the Project, and not to individual APIs (as it was the case before). When selecting an operation (i.e. a Query, Mutation, or Subscription), you can execute it against any of the APIs included in the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707149792683/b262a9dc-c357-41be-8b84-7fe0cf046ad5.gif" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">ℹ</div>
<div data-node-type="callout-text">For those of you coming from Postman, you can think of Projects as <em>workspaces</em>, where each API corresponds to an <em>environment</em>.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Tip: GraphBolt lets you include a specific list of APIs or <em>all</em> APIs from an account/region. For stable environments (e.g. above <code>dev</code>), I recommend selecting the exact API instances to include. For lower environments, keep the "<strong>All current and future APIs</strong>" toggle checked. This way, any newly created API will be automatically included.</div>
</div>

<h1 id="heading-tabbing-navigation">Tabbing Navigation 🗂</h1>
<p>Until now you were only able to work on one thing at a time, whether it was an operation, a resolver, etc. This was very limiting in some cases, like executing a Subscription followed by a Mutation to check if the message is delivered as expected. This has been a long-standing issue with GraphBolt.</p>
<p>Starting from this version, every "view" will open in a new tab, making it much easier to work with several things at the same time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707150390705/cb5fd220-121b-410b-9090-90267ad895d3.gif" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">GraphBolt lets you know (with a 🔴 badge when something happens in a tab (e.g. a new subscription message arrives).</div>
</div>

<h1 id="heading-request-history">Request History 🕓</h1>
<p>Did you know that GraphBolt has been silently keeping track of all your requests for a while now? (Don't worry everything is still stored on your machine, I don't see them). However, there was no way to access them from the UI. This is now a done thing! You'll find a new drop-down menu on top of the response panel that lets you see the execution history.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707153617094/0d8964e3-21a2-4d9d-becf-0a102b98447e.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-per-request-authentication">Per Request Authentication 🔒</h1>
<p>So far, the authentication settings have been global per API. This was a problem when some operations use a <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#using-additional-authorization-modes">different authentication method</a> than others. For example, you could have a "public" query named <code>getPost()</code> which uses the API key authorizer, while the <code>createPost()</code> mutation uses Cognito User Pools.</p>
<p>You can now use a different authentication method for every individual operation.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">ℹ</div>
<div data-node-type="callout-text">For now, you are still limited to one Cognito User across all operations, per API, though. I hope to be able to introduce support for several user profiles soon. (e.g. user, admin, etc.)</div>
</div>

<h1 id="heading-what-else">What Else?</h1>
<p>Those are by far my favorite features, but I did not stop there. This new version also includes some bug fixes and other smaller improvements, notably in the UI and the onboarding process.</p>
<h1 id="heading-whats-next">What's Next?</h1>
<p>There are plenty of other great features I'd like to add that I will work on in the following weeks/month. What features would YOU like to see? <a target="_blank" href="mailto:benoit@graphbolt.dev">Let me know!</a></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>This new release is one of the biggest ever. Projects and tabs were much-needed features and I truly hope that they will boost your AppSync development workflow.</p>
<p><a target="_blank" href="https://graphbolt.dev/">Start using it now</a>, and don't hesitate to send me your questions and feedback.</p>
]]></content:encoded></item><item><title><![CDATA[What's new in GraphBolt v0.7.0?]]></title><description><![CDATA[At GraphBolt, we are on a mission: Pack everything you need to build, test, manage, and observe your AWS AppSync APIs in one single desktop application. 
Today, we are one step closer to that. We just released a new version of GraphBolt with three ne...]]></description><link>https://blog.graphbolt.dev/whats-new-in-graphbolt-v070</link><guid isPermaLink="true">https://blog.graphbolt.dev/whats-new-in-graphbolt-v070</guid><category><![CDATA[AppSync]]></category><category><![CDATA[aws appsync]]></category><category><![CDATA[AWS AppSync Introduction]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[#graphql-tools]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Sat, 16 Dec 2023 16:35:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1702744193032/e9d65f77-e4bb-4bfb-83ce-d3ec852d137e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At GraphBolt, we are on a mission: Pack everything you need to build, test, manage, and observe your AWS AppSync APIs in one single desktop application. </p>
<p>Today, we are one step closer to that. We just released a new version of GraphBolt with three new amazing tools.</p>
<h2 id="heading-1-api-details">1. API details ℹ️</h2>
<p>In the new <em>Details</em> tab, you will find basic information about your APIs such as the GraphQL endpoints (for queries and real-time), the ARN, tags, etc.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702743553886/00c3ad94-f91a-4c59-99f5-cafd3c88b1db.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-2-metrics">2. Metrics 📈</h2>
<p>GraphBolt is committed to making developers' lives easier, but once your APIs hit production, you want to keep an eye on how they perform, right?</p>
<p>GraphBolt now features CloudWatch metrics. Number of Requests, Errors, Latency, Consumed tokens, Caching, and even the <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2023/11/aws-appsync-metrics-monitoring-graphql-subscriptions/">new Real-time metrics</a>. It's all there!</p>
<p><img src="https://pbs.twimg.com/media/GBezL5kWkAAHu4p?format=jpg&amp;name=large" alt="Image" /></p>
<h2 id="heading-3-stats">3. Stats 📊</h2>
<p>Have you ever wondered which one of your AppSync resolvers is the most invoked? Or which one is the slowest? Maybe there is something you can do about it and improve latency and UX.</p>
<p>Under <em>Statistics</em>, you will find charts showing the most invoked resolvers and their execution time (average, min, and max). You can even choose a time range from which you would like to get data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702743550190/1154d6f3-5f92-43ba-84c0-c8a528a399f5.png" alt class="image--center mx-auto" /></p>
<p>All these features are available for <a target="_blank" href="https://graphbolt.dev?utm_campaign=v0.7.0&amp;utm_medium=Blog&amp;utm_source=blog.graphbolt.dev">download</a> today!</p>
<p>GraphBolt is still in beta and as such, it remains free to use for everyone.</p>
]]></content:encoded></item><item><title><![CDATA[Debugging AWS AppSync APIs With CloudWatch]]></title><description><![CDATA[AWS AppSync is a fully managed service by Amazon Web Services that allows developers to create secure and scalable GraphQL APIs easily.
One of the great benefits of AppSync is the ability to connect GraphQL schemas to data sources like DynamoDB by wr...]]></description><link>https://blog.graphbolt.dev/debugging-aws-appsync-apis-with-cloudwatch</link><guid isPermaLink="true">https://blog.graphbolt.dev/debugging-aws-appsync-apis-with-cloudwatch</guid><category><![CDATA[AppSync]]></category><category><![CDATA[AWS]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[serverless]]></category><category><![CDATA[aws appsync]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Mon, 30 Oct 2023 20:52:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698324315779/6e65f7a3-ddf6-4cb5-8d47-ca35f8f73ff6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS AppSync is a fully managed service by Amazon Web Services that allows developers to create secure and scalable GraphQL APIs easily.</p>
<p>One of the great benefits of AppSync is the ability to connect GraphQL schemas to data sources like DynamoDB by writing simple functions called resolvers, without having to worry too much about data fetching logic. However, when things are not working as expected, it becomes necessary to understand how those resolvers interact with your data sources and the GraphQL schema in order to fix the problem.</p>
<p>Like many other services, AppSync gives you the option to send logs into AWS CloudWatch. When enabled, AppSync writes records describing how resolvers interact with the schema and the data sources.</p>
<p>In this article, I will teach you how to find your way in those logs and understand them so you can effectively diagnose issues.</p>
<h2 id="heading-enabling-the-logs">Enabling the logs</h2>
<p>Before you can dive deep into the logs, you must first enable verbose logging in your API settings. Settings under Logging should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696878151101/5b0a94ed-f98a-43cb-ab35-9e0fa658c09f.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Enable Logging is on ✅</p>
</li>
<li><p>"Include request headers, response headers, context, and evaluated mapping templates, regardless of field logging level": is checked ✅</p>
</li>
<li><p>Field resolver log level: <code>All</code></p>
</li>
</ul>
<p>You can also use your favorite IaC to do so.</p>
<p>If everything is set up properly, you should start seeing logs in CloudWatch after you execute a query.</p>
<blockquote>
<p>ℹ️ AWS AppSync Log Groups are usually named as follows: <code>/aws/appsync/apis/{apiId}</code></p>
</blockquote>
<h2 id="heading-the-anatomy-of-the-appsync-log-groups">The anatomy of the AppSync log groups</h2>
<p>AppSync logs provide very useful information about each step of the execution of the request. Most of the logs are JSON formatted, which makes them easy to parse, read, and search. Let's get to know them:</p>
<h3 id="heading-request-logs">Request logs</h3>
<p>At the top and bottom of the log stack, you will find some general details about the executed request.</p>
<ul>
<li><code>GraphQL Query</code></li>
</ul>
<p>This line gives you information about the query that was executed. It comprises the query itself, the operation name (if any), and the variables (if any). It is prefixed with the request ID.</p>
<p>Example</p>
<pre><code class="lang-plaintext">f89cbf60-2431-44ca-a6f5-44eec32e42e6 GraphQL Query: query GetPost($postId: ID!) {
  getPost(id: $postId) {
    id
    title
    content
    author {
      name
    }
  }
}, Operation: GetPost, Variables: {"postId":"22d38480-ca09-4d6d-9582-428c15af0bb2"}
</code></pre>
<ul>
<li><code>RequestSummary</code></li>
</ul>
<p>It provides a summary of the execution among which the <code>status</code> (the HTTP response code) and <code>latency</code> expressed in nanoseconds.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"logType"</span>: <span class="hljs-string">"RequestSummary"</span>,
  <span class="hljs-attr">"requestId"</span>: <span class="hljs-string">"f89cbf60-2431-44ca-a6f5-44eec32e42e6"</span>,
  <span class="hljs-attr">"graphQLAPIId"</span>: <span class="hljs-string">"seiizsmq2zfsfk5nkxfl4h46j4"</span>,
  <span class="hljs-attr">"statusCode"</span>: <span class="hljs-number">200</span>,
  <span class="hljs-attr">"latency"</span>: <span class="hljs-number">101516439</span>
}
</code></pre>
<ul>
<li><code>Request Headers</code></li>
</ul>
<p>The request headers of the HTTP request. Prefixed with the request ID.</p>
<pre><code class="lang-plaintext">f89cbf60-2431-44ca-a6f5-44eec32e42e6 Request Headers: {content-length=[228], cloudfront-viewer-country=[FR], sec-fetch-site=[cross-site], x-amzn-requestid=[f89cbf60-2431-44ca-a6f5-44eec32e42e6], x-amz-user-agent=[aws-amplify/3.0.7], x-forwarded-port=[443], via=[2.0 2f66f74411c5a2447c09372eb79e674e.cloudfront.net (CloudFront)], authorization=[****m3s1Lw], sec-ch-ua-mobile=[?0], cloudfront-viewer-asn=[12322], cloudfront-is-desktop-viewer=[true], host=xxxxx.appsync-api.us-east-1.amazonaws.com], content-type=[application/json], sec-fetch-mode=[cors], x-forwarded-proto=[https], accept-language=[en-GB], x-forwarded-for=[88.162.xxx.xxx, 18.68.xxx.xxx], accept=[*/*], cloudfront-is-smarttv-viewer=[false], sec-ch-ua=[" Not A;Brand";v="99", "Chromium";v="104"], x-amzn-trace-id=[Root=1-65245176-4d851b414d05bec92fc373de], cloudfront-is-tablet-viewer=[false], sec-ch-ua-platform=["macOS"], x-amzn-remote-ip=[xx.xxx.xx.xxx], cloudfront-forwarded-proto=[https], accept-encoding=[gzip, deflate, br], x-amz-cf-id=[839VZnCL8GNXiA_RIql-x5zBvjIb2RWGgQW1H3hr4V8sYqQKnC2Vsg==], user-agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) GraphBolt/0.4.0 Chrome/104.0.5112.102 Electron/20.1.0 Safari/537.36], cloudfront-is-mobile-viewer=[false], x-amzn-appsync-is-vpce-request=[false], sec-fetch-dest=[empty]}
</code></pre>
<p>Note: For security reasons, <code>authorization</code> and other sensitive values are obfuscated in CloudWatch. In this example, I have also hidden other private information myself.</p>
<ul>
<li><code>Response Headers</code></li>
</ul>
<p>The response headers of the HTTP request. Prefixed with the request ID.</p>
<pre><code class="lang-plaintext">f89cbf60-2431-44ca-a6f5-44eec32e42e6 Response Headers: {X-Amzn-Trace-Id=Root=1-65245176-4d851b414d05bec92fc373de, Content-Type=application/json; charset=UTF-8}
</code></pre>
<blockquote>
<p>💡 Tip: If X-Ray is enabled in the AppSync settings, <code>X-Amzn-Trace-Id</code> is the trace id. You can easily copy it to find the x-ray traces.</p>
</blockquote>
<ul>
<li><code>Tokens Consumed</code></li>
</ul>
<p>The total number of tokens consumed by the request. For more info about tokens, <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html#aws-appsync-using-token-counts-to-optimize-requests">see the documentation</a>.</p>
<p>Also see: <a target="_blank" href="https://blog.graphbolt.dev/the-aws-appsync-limits-you-need-to-know">The AWS AppSync Limits You Need To Know</a></p>
<pre><code class="lang-plaintext">f89cbf60-2431-44ca-a6f5-44eec32e42e6 Tokens Consumed: 1
</code></pre>
<h3 id="heading-resolver-logs">Resolver logs</h3>
<p>Aside from the logs referring to the global execution of the request, you will also find a series of lines that relate to every resolver execution in the request. Those logs are in JSON format and provide information about the execution of the resolver's handler functions.</p>
<p>As a general rule, you will usually find a pair of records for each resolver execution (one resolver might be executed more than once): one for the <code>request</code> handler, and one for the <code>response</code> handler (a.k.a. request and response mapping templates). You can recognize them by the <code>logType</code> property: respectively <code>RequestFunctionEvaluation</code> and <code>ResponseFunctionEvaluation</code>, or <code>RequestMapping</code> and <code>ResponseMapping</code>, depending on the runtime (APPSYNC_JS or VTL).</p>
<p>For pipeline resolvers, the same applies to every function in the pipeline (i.e. you will find a pair of logs for every function execution). In addition to that, you'll see an additional pair corresponding to the <code>before</code> and <code>after</code> handlers, wrapping all the functions logs: <code>BeforeRequestFunctionEvaluation</code> and <code>AfterResponseFunctionEvaluation</code>(APPSYNC_JS), or <code>BeforeMapping</code> and <code>AfterMapping</code> (VTL).</p>
<p>Here are the relevant fields that you will find in the records and their meaning:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Field</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><code>graphQLAPIId</code></td><td>The ID of the AppSync API.</td></tr>
<tr>
<td><code>requestId</code></td><td>The ID of the request.</td></tr>
<tr>
<td><code>resolverArn</code></td><td>The ARN of the resolver.</td></tr>
<tr>
<td><code>path</code></td><td>The full path of the field being resolved, as an array.</td></tr>
<tr>
<td><code>parentType</code></td><td>The parent type of the field this resolver is attached to.</td></tr>
<tr>
<td><code>fieldName</code></td><td>The name of the field this resolver is attached to.</td></tr>
<tr>
<td><code>functionName</code></td><td>Pipeline functions only. The name of the pipeline function.</td></tr>
<tr>
<td><code>functionArn</code></td><td>Pipeline functions only. The arn of the pipeline function.</td></tr>
<tr>
<td><code>context</code></td><td>The <code>context</code> object as received by the handler. This includes the <code>arguments</code>, <code>source</code>, <code>stash</code>, <code>result</code>, etc. (<code>identity</code> is excluded for security reasons).</td></tr>
<tr>
<td><code>evaluationResult</code></td><td>JS resolvers only. The JSON object returned by the handler after evaluation.</td></tr>
<tr>
<td><code>transformedTemplate</code></td><td>VTL only. The result of the mapping template after evaluation by the VTL engine.</td></tr>
<tr>
<td><code>fieldInError</code></td><td>Whether the field is in error. This usually happens after you call <code>util.error()</code>, or if the Data Source request is invalid.</td></tr>
<tr>
<td><code>errors</code></td><td>A list of errors (as an array) that might have occurred during the handler evaluation. This includes custom errors (i.e. <code>util.error()</code>), or any error that might have occurred during the execution of the handler (e.g. invalid Data Source request). This would <strong>not</strong> include errors returned by the data source. (For example: <code>DynamoDB:ConditionalCheckFailedException</code>). Those are usually available under <code>context.error</code></td></tr>
</tbody>
</table>
</div><p>Here is an example of a full log record.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"logType"</span>: <span class="hljs-string">"RequestFunctionEvaluation"</span>,
  <span class="hljs-attr">"fieldName"</span>: <span class="hljs-string">"getPost"</span>,
  <span class="hljs-attr">"resolverArn"</span>: <span class="hljs-string">"arn:aws:appsync:us-east-1:379730309663:apis/seiizsmq2zfsfk5nkxfl4h46j4/types/Query/resolvers/getPost"</span>,
  <span class="hljs-attr">"functionName"</span>: <span class="hljs-string">"getPost"</span>,
  <span class="hljs-attr">"fieldInError"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"evaluationResult"</span>: {
    <span class="hljs-attr">"operation"</span>: <span class="hljs-string">"GetItem"</span>,
    <span class="hljs-attr">"key"</span>: {
      <span class="hljs-attr">"id"</span>: {
        <span class="hljs-attr">"S"</span>: <span class="hljs-string">"22d38480-ca09-4d6d-9582-428c15af0bb2"</span>
      }
    }
  },
  <span class="hljs-attr">"parentType"</span>: <span class="hljs-string">"Query"</span>,
  <span class="hljs-attr">"path"</span>: [
    <span class="hljs-string">"getPost"</span>
  ],
  <span class="hljs-attr">"requestId"</span>: <span class="hljs-string">"fea8bf05-19a9-4b82-b8fa-a854d8f29737"</span>,
  <span class="hljs-attr">"context"</span>: {
    <span class="hljs-attr">"arguments"</span>: {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"22d38480-ca09-4d6d-9582-428c15af0bb2"</span>
    },
    <span class="hljs-attr">"prev"</span>: {
      <span class="hljs-attr">"result"</span>: {}
    },
    <span class="hljs-attr">"stash"</span>: {},
    <span class="hljs-attr">"outErrors"</span>: []
  },
  <span class="hljs-attr">"errors"</span>: [],
  <span class="hljs-attr">"graphQLAPIId"</span>: <span class="hljs-string">"seiizsmq2zfsfk5nkxfl4h46j4"</span>,
  <span class="hljs-attr">"functionArn"</span>: <span class="hljs-string">"arn:aws:appsync:us-east-1:379730309663:apis/seiizsmq2zfsfk5nkxfl4h46j4/functions/hdmi2bvayzh6zhoudhm6uc5voe"</span>
}
</code></pre>
<p>In the above example, we can see the record for the <code>request</code> handler of the <code>Query.getPost</code> resolver. Since it's in a pipeline resolver, we also see the <code>functionName</code> (<code>getPost</code>). We also find the query that was sent to the data source (DynamoDB) under <code>evaluationResult</code>.</p>
<h3 id="heading-tracing-logs">Tracing logs</h3>
<p>Tracing logs provide extra information about the resolution of a field, including those that are not attached to a resolver.</p>
<p>Example:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"duration"</span>: <span class="hljs-number">41818411</span>,
  <span class="hljs-attr">"logType"</span>: <span class="hljs-string">"Tracing"</span>,
  <span class="hljs-attr">"path"</span>: [
    <span class="hljs-string">"getPost"</span>
  ],
  <span class="hljs-attr">"fieldName"</span>: <span class="hljs-string">"getPost"</span>,
  <span class="hljs-attr">"startOffset"</span>: <span class="hljs-number">135988</span>,
  <span class="hljs-attr">"resolverArn"</span>: <span class="hljs-string">"arn:aws:appsync:us-east-1:379730309663:apis/seiizsmq2zfsfk5nkxfl4h46j4/types/Query/resolvers/getPost"</span>,
  <span class="hljs-attr">"requestId"</span>: <span class="hljs-string">"81f08e54-f589-4775-b96f-7bb8ed218539"</span>,
  <span class="hljs-attr">"parentType"</span>: <span class="hljs-string">"Query"</span>,
  <span class="hljs-attr">"returnType"</span>: <span class="hljs-string">"Post!"</span>,
  <span class="hljs-attr">"graphQLAPIId"</span>: <span class="hljs-string">"seiizsmq2zfsfk5nkxfl4h46j4"</span>
}
</code></pre>
<p>Most fields are identical to resolver logs; and here are the other interesting ones:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Field</td><td><strong>Description</strong></td></tr>
</thead>
<tbody>
<tr>
<td><code>startOffset</code></td><td>The time passed, in nanoseconds, between the beginning of the request and the beginning of the resolution of this field.</td></tr>
<tr>
<td><code>duration</code></td><td>The total duration of the field resolution, in nanoseconds.</td></tr>
<tr>
<td><code>returnType</code></td><td>The type of the field.</td></tr>
</tbody>
</table>
</div><h3 id="heading-custom-logs">Custom logs</h3>
<p>Did you know that you can write custom logs from your resolvers?</p>
<p>In a JavaScript resolver, you can call <code>console.log()</code> or <code>console.error()</code>. If you're still using VTL, use <code>$util.log.info()</code> or <code>$util.log.error()</code>. You will see them appear in CloudWatch along with the file name (<a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#additional-utilities">if you included a source map</a>), the line number, and the log level (<code>INFO</code> or <code>ERROR</code>). Use them to log custom data or debugging information.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Hello from a JS resolver request handler!!'</span>);
</code></pre>
<pre><code class="lang-plaintext">fea8bf05-19a9-4b82-b8fa-a854d8f29737 INFO - resolvers/getPost.ts:11:2: "Hello from a JS resolver request handler!!"
</code></pre>
<p>You can even write plain objects, and they will appear as JSONs.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log({ <span class="hljs-attr">hey</span>: <span class="hljs-string">'there'</span> });
</code></pre>
<pre><code class="lang-plaintext">2ab4b798-8941-47d1-9f66-453389e5d67c INFO - resolvers/getPost.ts:12:2: {"hey":"there"}
</code></pre>
<h2 id="heading-finding-the-right-logs">Finding the right logs</h2>
<p>AppSync logs in CloudWatch can be very (<a target="_blank" href="https://twitter.com/Benoit_Boure/status/1487507975484985344">very</a>) verbose, and it's not always easy to find what you're looking for among hundreds of lines of logs.</p>
<p>Luckily, AWS AppSync returns the request ID in the <code>x-amzn-requestid</code> HTTP response header.</p>
<p>If you're using an app like Postman to execute queries, you can easily find it and copy it (You can also find it in the Network tab of your browser's developer console). You can then paste it into CloudWatch to filter the logs of that particular request.</p>
<p>Note: You'll need to wrap the ID in quotes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697784289487/97055e59-c837-4480-88c7-de7cd53dec1d.png" alt class="image--center mx-auto" /></p>
<p>Alternatively, if you know exactly what you are looking for, you can also use a JSON filter.</p>
<p>For example, if you are interested in the <code>request</code> handler of the <code>getPost</code> query, for a request with the ID <code>278d0f86-83e8-417f-8985-6fa57ab7e5bd</code>, you can apply the following filter:</p>
<pre><code class="lang-json">{ $.requestId = <span class="hljs-attr">"278d0f86-83e8-417f-8985-6fa57ab7e5bd"</span>
  &amp;&amp; $.logType = <span class="hljs-attr">"RequestFunctionEvaluation"</span>
  &amp;&amp; $.fieldName = <span class="hljs-attr">"getPost"</span> }
</code></pre>
<blockquote>
<p>ℹ️ Note: Logs can take a few seconds to show up in CloudWatch. If you just executed the request, you might need to refresh a few times before you see them.</p>
</blockquote>
<h2 id="heading-debugging-requests">Debugging requests</h2>
<p>Once you understand how logs are structured, how they appear in CloudWatch, and how to find them, it becomes much easier to debug requests. Let's take a common mistake as an example.</p>
<p>Imagine you wrote the following JS resolver code:</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">return</span> {
    <span class="hljs-attr">operation</span>: <span class="hljs-string">'GetItem'</span>,
    <span class="hljs-attr">key</span>: util.dynamodb.toMapValues({
      <span class="hljs-attr">id</span>: ctx.arguments.postId,
    }),
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span>(<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">return</span> ctx.result;
}
</code></pre>
<p>You're sending this query.</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">query</span> {
  getPost(<span class="hljs-symbol">id:</span> <span class="hljs-string">"f9f621c6-d9da-4880-a148-69535a118494"</span>) {
    id
    title
    content
  }
}
</code></pre>
<p>And this is the GraphQL schema corresponding to that request.</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Query {
  getPost(<span class="hljs-symbol">id:</span> ID!): Post!
}
</code></pre>
<p>After executing the request, you receive this response:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"data"</span>: <span class="hljs-literal">null</span>,
  <span class="hljs-attr">"errors"</span>: [
    {
      <span class="hljs-attr">"path"</span>: [
        <span class="hljs-string">"getPost"</span>
      ],
      <span class="hljs-attr">"locations"</span>: <span class="hljs-literal">null</span>,
      <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Cannot return null for non-nullable type: 'Post' within parent 'Query' (/getPost)"</span>
    }
  ]
}
</code></pre>
<p>You are certain that the post exists, you checked in your DynamoDB table. So what is going on? Let's have a look at the logs. First, let's identify the <code>request</code> handler logs to see if we see anything. We're looking for a <code>RequestFunctionEvaluation</code> log for the <code>getPost</code> resolver.</p>
<p>After looking in CloudWatch, I found it:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"logType"</span>: <span class="hljs-string">"RequestFunctionEvaluation"</span>,
  <span class="hljs-attr">"path"</span>: [
    <span class="hljs-string">"getPost"</span>
  ],
  <span class="hljs-attr">"fieldName"</span>: <span class="hljs-string">"getPost"</span>,
  <span class="hljs-attr">"resolverArn"</span>: <span class="hljs-string">"arn:aws:appsync:us-east-1:111111111111:apis/seiizsmq2zfsfk5nkxfl4h46j4/types/Query/resolvers/getPost"</span>,
  <span class="hljs-attr">"requestId"</span>: <span class="hljs-string">"f9f621c6-d9da-4880-a148-69535a118494"</span>,
  <span class="hljs-attr">"context"</span>: {
    <span class="hljs-attr">"arguments"</span>: {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"22d38480-ca09-4d6d-9582-428c15af0bb2"</span>
    },
    <span class="hljs-attr">"stash"</span>: {},
    <span class="hljs-attr">"outErrors"</span>: []
  },
  <span class="hljs-attr">"fieldInError"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"evaluationResult"</span>: {
    <span class="hljs-attr">"operation"</span>: <span class="hljs-string">"GetItem"</span>,
    <span class="hljs-attr">"key"</span>: {
      <span class="hljs-attr">"id"</span>: {}
    }
  },
  <span class="hljs-attr">"errors"</span>: [],
  <span class="hljs-attr">"parentType"</span>: <span class="hljs-string">"Query"</span>,
  <span class="hljs-attr">"graphQLAPIId"</span>: <span class="hljs-string">"seiizsmq2zfsfk5nkxfl4h46j4"</span>
}
</code></pre>
<p>The first thing you would usually look at would be the <code>evaluationResult</code> which is the request that was sent to the data source (in this case DynamoDB). Here, something does not look right. You can see that the <code>key</code> value of the <code>GetItem</code> operation is empty. The ID is not sent correctly to DynamoDB.</p>
<p>We can confirm that by looking at the corresponding <code>ResponseFunctionEvaluation</code> record. DynamoDB complains about an invalid key (See <code>context.error</code>).</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"logType"</span>: <span class="hljs-string">"ResponseFunctionEvaluation"</span>,
  <span class="hljs-attr">"path"</span>: [
    <span class="hljs-string">"getPost"</span>
  ],
  <span class="hljs-attr">"fieldName"</span>: <span class="hljs-string">"getPost"</span>,
  <span class="hljs-attr">"resolverArn"</span>: <span class="hljs-string">"arn:aws:appsync:us-east-1:379730309663:apis/seiizsmq2zfsfk5nkxfl4h46j4/types/Query/resolvers/getPost"</span>,
  <span class="hljs-attr">"functionName"</span>: <span class="hljs-string">"getPost"</span>,
  <span class="hljs-attr">"requestId"</span>: <span class="hljs-string">"81f08e54-f589-4775-b96f-7bb8ed218539"</span>,
  <span class="hljs-attr">"context"</span>: {
    <span class="hljs-attr">"arguments"</span>: {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"22d38480-ca09-4d6d-9582-428c15af0bb2"</span>
    },
    <span class="hljs-attr">"prev"</span>: {
      <span class="hljs-attr">"result"</span>: {}
    },
    <span class="hljs-attr">"stash"</span>: {},
    <span class="hljs-attr">"error"</span>: {
      <span class="hljs-attr">"message"</span>: <span class="hljs-string">"The provided key element does not match the schema (Service: DynamoDb, Status Code: 400, Request ID: HUDP47TVTALEGM8NB90JVLK5RRVV4KQNSO5AEMVJF66Q9ASUAAJG)"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"DynamoDB:DynamoDbException"</span>
    },
    <span class="hljs-attr">"outErrors"</span>: []
  },
  <span class="hljs-attr">"fieldInError"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"errors"</span>: [],
  <span class="hljs-attr">"parentType"</span>: <span class="hljs-string">"Query"</span>,
  <span class="hljs-attr">"graphQLAPIId"</span>: <span class="hljs-string">"seiizsmq2zfsfk5nkxfl4h46j4"</span>,
  <span class="hljs-attr">"functionArn"</span>: <span class="hljs-string">"arn:aws:appsync:us-east-1:379730309663:apis/seiizsmq2zfsfk5nkxfl4h46j4/functions/hdmi2bvayzh6zhoudhm6uc5voe"</span>
}
</code></pre>
<p>This is a sign that something is wrong with the <code>request</code> handler. Let's double-check it, and pay attention to the <code>key</code> part.</p>
<pre><code class="lang-javascript">    key: util.dynamodb.toMapValues({
      <span class="hljs-attr">id</span>: ctx.arguments.postId,
    })
</code></pre>
<p>We're sending <code>ctx.arguments.postId</code>. Let's check the <code>arguments</code> that were received by the handler to confirm the value (They are in the <code>context</code> property).</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"arguments"</span>: {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"22d38480-ca09-4d6d-9582-428c15af0bb2"</span>
    },
    <span class="hljs-attr">"stash"</span>: {},
    <span class="hljs-attr">"outErrors"</span>: []
  }
</code></pre>
<p>If you look closely, there is a typo in the request handler. The GraphQL query (schema) takes <code>id</code> as an argument, but the request handler sends <code>postId</code> 🤦‍♂️.</p>
<p>The <code>request</code> handler should look like this</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">return</span> {
    <span class="hljs-attr">operation</span>: <span class="hljs-string">'GetItem'</span>,
    <span class="hljs-attr">key</span>: util.dynamodb.toMapValues({
      <span class="hljs-attr">id</span>: ctx.arguments.id,  <span class="hljs-comment">// &lt;-- fixed argument</span>
    }),
  };
}
</code></pre>
<p>After re-deploying, everything works as expected. 🎉</p>
<p>As you can see, the logs can help you easily identify issues due to invalid request/response handlers code. <code>context</code> and <code>evaluationResult</code> are very useful to see what happened inside the resolver and how it interacted with the data source.</p>
<blockquote>
<p>💡 Tip: With the use of <a target="_blank" href="https://blog.graphbolt.dev/improving-developer-experience-with-typescript-how-to-write-strongly-typed-appsync-resolvers">TypeScript to write AppSync resolvers</a>, it also becomes easier to detect those kinds of mistakes in advance.</p>
</blockquote>
<h1 id="heading-graphbolt">GraphBolt</h1>
<p>GraphBolt is a tool that makes it much easier for developers to test their AppSync APIs. It packs everything you need in one desktop application, including a powerful GraphQL client. It also makes it super easy to debug requests.</p>
<p>Let's take our previous use case, and see how GraphBolt would have helped us find the issue faster.</p>
<p>After executing the request directly from the app, I see an error in the response panel (1). At this point, I have no clue where the problem is.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698131488126/d13f0df6-5af6-4263-839b-e8d51fa2fa68.png" alt class="image--center mx-auto" /></p>
<p>I click on the debug button (2). After a few seconds, it shows information about the last executed request, including resolver evaluations and their data sources, x-ray traces, request and response headers, and CloudWatch logs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698503588765/9a16594c-7880-4d23-a521-8bc4e2e3d603.png" alt class="image--center mx-auto" /></p>
<p>I'm interested in the <code>getPost</code> resolver which did not return the expected result, so I click on it in the list to get more details. A modal opens. It contains information about the execution of the resolver.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698131662445/65b48974-4a89-4c2b-abce-a8d00e8e5f0c.png" alt="Resolver details" class="image--center mx-auto" /></p>
<p>By looking at the <code>getPost Request</code> tab, I can see the <code>context</code> object received by the request handler of my pipeline function on the left side; and its evaluation result on the right side. I can immediately see that the operation sent to DynamoDB is wrong.</p>
<p>The <code>getPost Response</code> tab also shows me the error returned by DynamoDB.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698131717035/fc12815d-2758-4631-98aa-52bf29f097eb.png" alt class="image--center mx-auto" /></p>
<p>All that, in just a few seconds, without leaving the app and going through CloudWatch logs 🙌</p>
<p>GraphBolt has <a target="_blank" href="https://docs.graphbolt.dev?utm_medium=Blog&amp;utm_source=blog.graphbolt.dev&amp;utm_content=debug-appsync-with-cloudwatch">plenty of other features</a> to help you test, and debug AppSync APIs. <a target="_blank" href="https://graphbolt.dev?utm_medium=Blog&amp;utm_source=blog.graphbolt.dev&amp;utm_content=debug-appsync-with-cloudwatch">Try it for free today</a>!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Debugging AppSync requests is not a free lunch, but with the right knowledge and tools, it becomes easier. In the article, I showed you how to decode AppSync CloudWatch logs and use them to debug GraphQL requests. I also showed you how GraphBolt can help you make this process even quicker and easier.</p>
<p>Happy GraphQL coding!</p>
<p><em>Image credits: Image generated with krea.ia, and edited with Adobe Firefly.</em></p>
]]></content:encoded></item><item><title><![CDATA[Unlock the Power of Event-Driven Architecture with AppSync's EventBridge Data Source]]></title><description><![CDATA[AppSync is a fully managed service provided by Amazon Web Services (AWS) that simplifies the process of building scalable APIs for web and mobile applications. It allows developers to easily create GraphQL APIs that can be connected to various data s...]]></description><link>https://blog.graphbolt.dev/unlock-the-power-of-event-driven-architecture-with-appsyncs-eventbridge-data-source</link><guid isPermaLink="true">https://blog.graphbolt.dev/unlock-the-power-of-event-driven-architecture-with-appsyncs-eventbridge-data-source</guid><category><![CDATA[AppSync]]></category><category><![CDATA[AWS EventBridge]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[Serverl]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Thu, 13 Apr 2023 06:19:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678197266845/d467467e-6f3a-4364-800f-72a0558319bf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AppSync is a fully managed service provided by Amazon Web Services (AWS) that simplifies the process of building scalable APIs for web and mobile applications. It allows developers to easily create GraphQL APIs that can be connected to various data sources, including AWS services like DynamoDB and Lambda functions.</p>
<p>The AppSync team recently added <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2023/04/aws-appsync-publishing-events-amazon-eventbridge/">support for EventBridge as a data source</a>. In this article, we will see how it works and how you can use it with a practical example.</p>
<h1 id="heading-how-it-works">How it works</h1>
<p>The EventBridge data source is a new addition that streamlines integration between AppSync and EventBridge. Previously, <a target="_blank" href="https://blog.iamjkahn.com/2019/12/invoking-even-more-aws-services-directly-from-aws-appsync.html">events could be sent to EventBridge via an HTTP data source</a>, but that required more complex resolvers, better knowledge of the events API, and more IaC to sign HTTP requests with sigv4. Now, with the new native EventBridge data source, AWS AppSync automatically handles all of the complex tasks, leaving you to simply write a simple resolver request operation.</p>
<p>Here is what a request looks like:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-attr">operation</span>: <span class="hljs-string">'PutEvents'</span>,
  <span class="hljs-attr">events</span>: [
    {
      <span class="hljs-attr">source</span>: <span class="hljs-string">'AppSync'</span>, <span class="hljs-comment">// it can be anything</span>
      <span class="hljs-attr">detailType</span>: <span class="hljs-string">'myEvent'</span>,
      <span class="hljs-attr">detail</span>: {
        <span class="hljs-attr">foo</span>: <span class="hljs-string">'bar'</span>,
      },
    },
  ],
}
</code></pre>
<p>You can put up to 10 events in the same request. Just pass them as an array in the <code>events</code> property.</p>
<p>Note that the EventBridge Data "Source" is a bit special in that it's write-only. The only operation you can do is <code>PutEvents</code>.</p>
<p>The response to the request returns information relative to the events that were put on the bus.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Entries"</span>: [
    {
      <span class="hljs-attr">"EventId"</span>: <span class="hljs-string">"3298e5f9-66a6-ece5-f0c7-9ff791bcfd1e"</span>
    }
  ],
  <span class="hljs-attr">"FailedEntryCount"</span>: <span class="hljs-number">0</span>
}
</code></pre>
<blockquote>
<p>📚 For more information, <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-eventbridge-js.html">see the documentation</a></p>
</blockquote>
<h1 id="heading-a-practical-example">A Practical Example</h1>
<p>Great, but how can it be used? Let's take an example.</p>
<p>Imagine that you are building a blog application. Each time that someone publishes a new blog post, you want to notify the followers of the author via email to let them know about it (Similar to <a target="_blank" href="https://support.hashnode.com/en/articles/6427569-newsletters">Hashnode's Newsletter feature</a>).</p>
<p>One way to do this is by using an Event-Driven approach. EventBridge is often associated with <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-1">Event-Driven applications</a>. This involves publishing an event, such as <code>postCreated</code> to an event bus after the post is created. Next, you can set up an EventBridge rule that triggers a Lambda function or a Step Function workflow. This workflow can then retrieve all subscribers for the author, construct an email, and send it out.</p>
<p>Let's have a look at how we can implement this using AppSync pipeline resolvers and the new EventBridge data source integration.</p>
<p>To achieve this, we are going to use two functions in our pipeline. Let's call them <code>createPost</code> and <code>putEvent</code>.</p>
<p>The first step is <code>createPost</code>, which utilizes a DynamoDB data source. With the use of a <code>PutItem</code> operation, it inserts the post into a table. Following it, <code>putEvent</code> takes advantage of the new EventBridge integration to place an event on the event bus.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678192982956/84f6454e-7605-4e9c-acd0-682cbc9901d2.png" alt="CreatePost Pipeline resolver overview" class="image--center mx-auto" /></p>
<p>The handlers are as follows:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// createPost.js</span>
<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">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> item = {
    ...ctx.args.post,
    <span class="hljs-attr">createdAt</span>: util.time.nowISO8601(),
    <span class="hljs-attr">updatedAt</span>: util.time.nowISO8601(),
  };
  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">operation</span>: <span class="hljs-string">'PutItem'</span>,
    <span class="hljs-attr">key</span>: {
      <span class="hljs-attr">id</span>: util.dynamodb.toDynamoDB(util.autoId()),
    },
    <span class="hljs-attr">attributeValues</span>: util.dynamodb.toMapValues(item),
    <span class="hljs-attr">condition</span>: {
      <span class="hljs-attr">expression</span>: <span class="hljs-string">'attribute_not_exists(#id)'</span>,
      <span class="hljs-attr">expressionNames</span>: {
        <span class="hljs-string">'#id'</span>: <span class="hljs-string">'id'</span>,
      },
    },
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span>(<span class="hljs-params">ctx</span>) </span>{
  ctx.stash.event = {
    <span class="hljs-attr">detailType</span>: <span class="hljs-string">'postCreated'</span>,
    <span class="hljs-attr">detail</span>: ctx.result,
  };

  <span class="hljs-keyword">return</span> ctx.result;
}
</code></pre>
<p>In the <code>response</code> handler, we first build the event that is going to be put into EventBridge. It contains the <code>detailType</code> (<code>postCreated</code>) and the <code>detail</code> information which is the post item that was just inserted into DynamoDB. Before we return, we <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html">stash</a> it. The returned value of the resolver is the post itself.</p>
<p>Pre-constructing the event in <code>createPost</code> allows us to keep the <code>putEvent</code> function more generic and reusable. i.e. It only has to call <code>PutEvents</code> with the data coming from the previous step (stashed) and does not have to deal with specificities.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// putEvent.js</span>
<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">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">if</span> (!ctx.stash.event) {
    util.error(<span class="hljs-string">'InternalError'</span>, <span class="hljs-string">'Event missing in stash'</span>);
  }

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">operation</span>: <span class="hljs-string">'PutEvents'</span>,
    <span class="hljs-attr">events</span>: [
      {
        <span class="hljs-attr">source</span>: <span class="hljs-string">'blog-appsync-api'</span>,
        ...ctx.stash.event,
      },
    ],
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span>(<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">if</span> (ctx.error) {
    util.error(<span class="hljs-string">'Failed putting event in EventBride'</span>, <span class="hljs-string">'Error'</span>);
  }

  <span class="hljs-keyword">return</span> ctx.prev.result;
}
</code></pre>
<p>The response handler returns the result from the previous function, that is the post item that was inserted. This is also the value that is returned by the pipeline resolver to the client. The EventBridge step is completely transparent.</p>
<p>Let's test it.</p>
<p><a target="_blank" href="https://graphbolt.dev/?utm_campaign=event-bridge-integration&amp;utm_medium=Referral&amp;utm_source=blog.graphbolt.dev">GraphBolt</a> is a tool that helps developers build and test AppSync APIs. Thanks to the embedded GraphQL client, you can easily execute queries and mutations. I'm going to use it to call the <code>createPost</code> Mutation.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678200679829/c5a35b52-757a-4b23-a12f-67c21b44ad35.png" alt="The Mutation returned the inserted post, proving that the resolver works as expected." class="image--center mx-auto" /></p>
<p>The Mutation returned the inserted post, meaning that the resolver works as expected. But how about the event?</p>
<p>GraphBolt also comes with a <a target="_blank" href="https://docs.graphbolt.dev/query-inspector/resolver-details">debugger</a> that lets you see what happened under the hood of a resolver. Let's check it out.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678201038472/17761e47-748f-4ebc-8e28-561762c87517.png" alt class="image--center mx-auto" /></p>
<p>As you can see, the <code>putEvent</code> function was invoked, and it put an event into EventBridge with the content of the post, as expected (right side of the image).</p>
<p>To double-check it, I also created an EventBridge rule that triggers a Lambda function and prints the event into CloudWatch.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678201282563/f8496560-2fb3-4c69-8538-b207a3fd9162.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>💡 You can find a fully functional example of this <a target="_blank" href="https://github.com/graphboltdev/appsync-event-bridge-datasource">on GitHub</a>, using the Serverless Framework.</p>
</blockquote>
<h1 id="heading-conclusion">Conclusion</h1>
<p>In this article, we learned about the new EventBridge data source integration in AWS AppSync. We saw how this integration streamlines the process of building event-driven applications by allowing developers to easily publish events from AppSync resolvers to EventBridge. We also saw a practical example of how to implement this. Overall, this new feature enhances the capabilities of AWS AppSync and provides developers with a powerful tool for building modern serverless APIs.</p>
]]></content:encoded></item><item><title><![CDATA[Improving Developer Experience with TypeScript: How to Write Strongly Typed AppSync Resolvers]]></title><description><![CDATA[AWS AppSync is a fully managed service that simplifies the process of building GraphQL APIs by handling the heavy lifting of securely connecting to data sources like AWS DynamoDB, AWS Lambda, and more. With AppSync, developers can easily create scala...]]></description><link>https://blog.graphbolt.dev/improving-developer-experience-with-typescript-how-to-write-strongly-typed-appsync-resolvers</link><guid isPermaLink="true">https://blog.graphbolt.dev/improving-developer-experience-with-typescript-how-to-write-strongly-typed-appsync-resolvers</guid><category><![CDATA[AppSync]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Tue, 11 Apr 2023 06:08:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678373761087/664e05d1-501f-477c-9ee9-680aadd3eb70.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS AppSync is a fully managed service that simplifies the process of building GraphQL APIs by handling the heavy lifting of securely connecting to data sources like AWS DynamoDB, AWS Lambda, and more. With AppSync, developers can easily create scalable and real-time applications.</p>
<p>In a previous article, I showed how you can <a target="_blank" href="https://blog.graphbolt.dev/write-reusable-code-for-appsync-javascript-resolvers">bundle AppSync JavaScript resolvers</a> and improve your Developer Experience by writing reusable code. In this issue, we'll see how we can take it one step further: write AppSync JavaScript resolvers in TypeScript.</p>
<p>TypeScript is a superset of JavaScript that adds static typing and other features to the language. Together with GraphQL, which is also typed, they are the perfect duo.</p>
<p>Here is what we'd like to achieve:</p>
<ul>
<li><p>Auto-generation of TypeScript types from a GraphQL SDL (Schema Definition Language)</p>
</li>
<li><p>Use the generated types along with generic AppSync types in resolver handlers</p>
</li>
<li><p>Transpile and bundle TypeScript into valid AppSync JavaScript code (<code>APPSYNC_JS</code> runtime)</p>
</li>
<li><p>Deploy an AppSync API with the TypeScript CDK</p>
</li>
</ul>
<blockquote>
<p>💡 You can find the complete code used in this article <a target="_blank" href="https://github.com/graphboltdev/appsync-typescript-resolvers">on GitHub</a></p>
</blockquote>
<h1 id="heading-typescript-codegen">TypeScript Codegen</h1>
<p>With the help of the <a target="_blank" href="https://the-guild.dev/graphql/codegen">GraphQL codegen</a> tool, you can easily generate TypeScript types from GraphQL schemas. I previously wrote about how you can <a target="_blank" href="https://benoitboure.com/how-to-use-typescript-with-appsync-lambda-resolvers">generate TypeScript types for AppSync Lambda resolvers</a>. The good news is that the process is exactly the same; so I won't go into details here. If you want to know more, please refer to that article.</p>
<p>TL;DR;</p>
<p>Given <a target="_blank" href="https://github.com/graphboltdev/appsync-typescript-resolvers/blob/master/schema.graphql">this GraphQL schema</a> and <a target="_blank" href="https://github.com/graphboltdev/appsync-typescript-resolvers/blob/master/codegen.ts">this codegen</a> file, we obtain the <a target="_blank" href="https://github.com/graphboltdev/appsync-typescript-resolvers/blob/master/src/types/appsync.ts">generated types</a> after running the following command:</p>
<pre><code class="lang-bash">graphql-codegen
</code></pre>
<p>Side note: An alternative option is to use the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#working-with-amplify-codegen">Amplify CLI</a>.</p>
<blockquote>
<p>💡 Tip: I recommend that you commit the generated type files to the repository. Just don't forget to re-generate them when your schema changes!</p>
</blockquote>
<h1 id="heading-writing-resolvers-in-typescript">Writing Resolvers In TypeScript</h1>
<p>Out of the box, the AppSync <a target="_blank" href="https://www.npmjs.com/package/@aws-appsync/utils">utils package</a> (<code>@aws-appsync/utils</code>) comes with a very convenient set of types that we can use as a base for our resolvers.</p>
<p>One of them, and probably the most relevant one, is <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference-js.html"><code>Context</code></a>, which provides the definition for the first argument of the <code>request</code> and <code>response</code> handler functions (usually named <code>context</code> or <code>ctx</code>).</p>
<p>This type takes arguments to specify the shape of the different use case-specific properties, namely: <code>args</code> (or <code>arguments</code>), <code>stash</code>, <code>prev</code>, <code>source</code> and <code>result</code>. We can use it combined with the types we generated earlier from the schema to unlock all the benefits of type-safety and IntelliSense.</p>
<p>Let's take an example. This is the <code>createPost</code> resolver from my <a target="_blank" href="https://github.com/graphboltdev/appsync-typescript-resolvers/">demo project</a>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Context, DynamoDBPutItemRequest, util } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-appsync/utils'</span>;
<span class="hljs-keyword">import</span> { createItem } <span class="hljs-keyword">from</span> <span class="hljs-string">'../lib/helpers'</span>;
<span class="hljs-keyword">import</span> { Post, MutationCreatePostArgs } <span class="hljs-keyword">from</span> <span class="hljs-string">'../types/appsync'</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: Context&lt;MutationCreatePostArgs&gt;,
</span>): <span class="hljs-title">DynamoDBPutItemRequest</span> </span>{
  <span class="hljs-comment">// add timestamps</span>
  <span class="hljs-keyword">const</span> item = createItem(ctx.args.post);

  <span class="hljs-keyword">return</span> {
    operation: <span class="hljs-string">'PutItem'</span>,
    key: {
      id: util.dynamodb.toDynamoDB(util.autoId()),
    },
    attributeValues: util.dynamodb.toMapValues({
      publishDate: util.time.nowISO8601(),
      ...item,
    }),
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span>(<span class="hljs-params">ctx: Context&lt;MutationCreatePostArgs, <span class="hljs-built_in">object</span>, <span class="hljs-built_in">object</span>, <span class="hljs-built_in">object</span>, Post&gt;</span>) </span>{
  <span class="hljs-keyword">return</span> ctx.result;
}
</code></pre>
<p>The usage of <code>Context&lt;MutationCreatePostArgs&gt;</code> in the request handler's argument provides us with a fully typed context object, where <code>args</code> corresponds to the mutation's input type generated from the SDL (in this case it's <code>MutationCreatePostArgs</code>).</p>
<p>Additionally, note that the request handler returns the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html#js-aws-appsync-resolver-reference-dynamodb-putitem">DynamoDBPutItemRequest</a> type, which is also provided by the AppSync utils package and defines the schema for a DynamoDB resolver's PutItem request.</p>
<p>Finally, in the response handler, I used <code>Context&lt;MutationCreatePostArgs, object, object, object, Post&gt;</code> to specify the type of the <code>result</code> property.</p>
<p>You'll find type declarations for all the available Data Sources, <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/built-in-util-js.html">built-in utilities</a>, and more!</p>
<h1 id="heading-transpiling-and-bundling">Transpiling and Bundling</h1>
<p>Writing AppSync resolvers in TypeScript greatly improves Developer Experience, but you can't just send TypeScript code to AppSync. Just like when you write Lambda functions in TypeScript, you need to transpile them to JavaScript first. For AppSync resolvers, the code also needs to be bundled.</p>
<p>Unfortunately, as of writing these lines, the CDK is still incapable of transpiling and bundling code automatically. Hopefully, <a target="_blank" href="https://github.com/aws/aws-cdk/pull/24986">this will improve over time</a>, but for now, we'll have to do it manually.</p>
<p>For that, we're going to use <a target="_blank" href="https://esbuild.github.io/">esbuild</a>. If you read the previous article about <a target="_blank" href="https://blog.graphbolt.dev/write-reusable-code-for-appsync-javascript-resolvers">bundling AppSync JavaScript resolvers</a>, the command is almost the same, except that in this case, esbuild will also take care of transpiling TypeScript to JavaScript.</p>
<p>Let's review the command:</p>
<pre><code class="lang-bash">esbuild src/resolvers/*.ts \
  --bundle \
  --outdir=build \
  --external:<span class="hljs-string">"@aws-appsync/utils"</span> \
  --format=esm \
  --platform=node \
  --target=esnext \
  --sourcemap=inline \
  --sources-content=<span class="hljs-literal">false</span>
</code></pre>
<ul>
<li><p><code>src/resolvers/*.ts</code> takes every file ending in <code>*.ts</code> in the <code>src/resolver</code> folder as entry points.</p>
</li>
<li><p><code>--bundle</code> bundles every entry point into one single file.</p>
</li>
<li><p><code>--outdir=build</code> is where the output is written. Each entry file will generate one output.</p>
</li>
<li><p><code>--external:"@aws-appsync/utils"</code> ignores the AppSync utils package from the bundle (It is provided by AppSync at runtime).</p>
</li>
<li><p><code>--format=esm</code> generates the output as ES modules (this is an AppSync requirement).</p>
</li>
<li><p><code>--platform=node</code> AppSync runs in an environment similar to NodeJS (i.e. not a browser)</p>
</li>
<li><p><code>--target=esnext</code> AppSync uses the latest ES module version.</p>
</li>
<li><p><code>--sourcemap=inline</code> AppSync supports <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#source-maps">source maps</a>, but they must be inline in the bundled file.</p>
</li>
<li><p><code>--sources-content=false</code> Do not include content in the source map.</p>
</li>
</ul>
<blockquote>
<p>ℹ️ Source maps are optional, but they are useful if you want to see references to your source files in logs and runtime error messages.</p>
</blockquote>
<h1 id="heading-deploying">Deploying</h1>
<p>Using the esbuild command works and is fine for testing and debugging purposes, but ideally, we'd like the code to be bundled automatically at deployment time. In this demo, I'm going to use the CDK (the TypeScript version of course).</p>
<p>Luckily, esbuild comes with a <a target="_blank" href="https://esbuild.github.io/api/#js-sync">JavaScript API</a>. We're going to use it in order to pre-build the resolvers directly from the CDK stack. Let's first write a helper function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> bundleAppSyncResolver = (entryPoint: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">Code</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> result = esbuild.buildSync({
    entryPoints: [entryPoint],
    external: [<span class="hljs-string">'@aws-appsync/utils'</span>],
    bundle: <span class="hljs-literal">true</span>,
    write: <span class="hljs-literal">false</span>,
    platform: <span class="hljs-string">'node'</span>,
    target: <span class="hljs-string">'esnext'</span>,
    format: <span class="hljs-string">'esm'</span>,
    sourcemap: <span class="hljs-string">'inline'</span>,
    sourcesContent: <span class="hljs-literal">false</span>,
  });

  <span class="hljs-keyword">return</span> Code.fromInline(result.outputFiles[<span class="hljs-number">0</span>].text);
};
</code></pre>
<p>The function takes a file path and calls esbuild's <code>buildSync</code> method with the same parameters as the command we executed earlier. We also add <code>write: false</code>, which just asks esbuild to return the code as a value, instead of writing it to disk. Finally, we return the transpiled and bundled code inline.</p>
<p>We can now use it in our stack definition:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// createPost resolver function</span>
<span class="hljs-keyword">const</span> createPost = <span class="hljs-keyword">new</span> appsync.AppsyncFunction(<span class="hljs-built_in">this</span>, <span class="hljs-string">'CreatePost'</span>, {
  name: <span class="hljs-string">'createPost'</span>,
  api,
  dataSource: postsDS,
  <span class="hljs-comment">// transpile, bundle and pass the code inline</span>
  code: bundleAppSyncResolver(<span class="hljs-string">'src/resolvers/createPost.ts'</span>),
  runtime: appsync.FunctionRuntime.JS_1_0_0,
});
</code></pre>
<p>You can find the complete code of the stack <a target="_blank" href="https://github.com/graphboltdev/appsync-typescript-resolvers/blob/master/lib/cdk-stack.ts">on GitHub</a>.</p>
<p>Deploying the stack works as usual:</p>
<pre><code class="lang-bash">cdk deploy
</code></pre>
<blockquote>
<p>ℹ️ Inline code is <a target="_blank" href="https://github.com/aws/aws-cdk/blob/v2.72.0/packages/@aws-cdk/aws-appsync/lib/code.ts#L38">limited to 4KiB</a>. If you hit that limit, you can pre-budle your code using the esbuild CLI, and point to the generated js file: <code>Code.fromAsset('</code>build/resolvers/createPost.js<code>').</code></p>
</blockquote>
<h1 id="heading-conclusion">Conclusion</h1>
<p>By using TypeScript to write AppSync resolvers, developers can benefit from static typing and enjoy IntelliSense capabilities without the need of relying on Lambda functions. The use of codegen effortlessly provides all the types that are specific to the AP's schema, which can later be used in combination with built-in AppSync types. And with the help of esbuild, the code can easily be transpiled and bundled into valid <code>APPSYNC_JS</code> runtime.</p>
]]></content:encoded></item><item><title><![CDATA[The AWS AppSync Limits You Need To Know]]></title><description><![CDATA[AWS AppSync is a managed service that enables developers to build scalable and secure GraphQL APIs quickly. However, as with any service, there are certain limits that must keep in mind while designing their APIs. In this article, we'll discuss the i...]]></description><link>https://blog.graphbolt.dev/the-aws-appsync-limits-you-need-to-know</link><guid isPermaLink="true">https://blog.graphbolt.dev/the-aws-appsync-limits-you-need-to-know</guid><category><![CDATA[AppSync]]></category><category><![CDATA[aws appsync]]></category><category><![CDATA[serverless]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Mon, 27 Mar 2023 07:00:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679694406509/2cc914a0-742f-4863-8a20-cd20062470f3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS AppSync is a managed service that enables developers to build scalable and secure GraphQL APIs quickly. However, as with any service, there are certain limits that must keep in mind while designing their APIs. In this article, we'll discuss the important limits you should know.</p>
<h2 id="heading-api-limits">API Limits</h2>
<p>You can create up to 25 AppSync APIs per region. However, if you need more, this limit can be increased. Furthermore, each API can have a maximum of 50 API keys, and up to 50 <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html">authentication providers</a>. Those can't be extended, but they should be more than enough to cover most use cases.</p>
<h2 id="heading-execution-time">Execution Time</h2>
<p>AWS AppSync limits the execution time for mutations, queries, and subscriptions to 30 seconds. This limit ensures that the API responds to requests in a timely manner.</p>
<p>Queries that last too long provide a very bad user experience for your customers. There are several things you can do to reduce the execution time:</p>
<ul>
<li><p>When possible, prefer VTL or JavaScript resolvers over Lambda functions, it can save you some cold starts.</p>
</li>
<li><p>Avoid resolvers that are too deeply nested. Nested resolvers are executed in series because child resolvers expect the parent's data (<code>ctx.source</code>).</p>
</li>
<li><p>If you're using pipeline resolvers, keep the number of functions to a minimum.</p>
</li>
<li><p>Optimize your requests. For example, make sure you use proper indexes when you query DynamoDB or RDS data sources.</p>
</li>
</ul>
<blockquote>
<p>💡 Tools such as <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/x-ray-tracing.html">X-Ray</a> or <a target="_blank" href="https://www.graphbolt.dev/?utm_medium=Blog&amp;utm_source=blog.graphbolt.dev&amp;utm_content=appsync-limits">GraphBolt</a> can help you understand how queries are executed and detect slow resolvers and bottlenecks.</p>
</blockquote>
<h2 id="heading-resolvers">Resolvers</h2>
<p>The maximum number of resolvers that can be executed in a single request is 10,000. Remember that one resolver might be executed more than once in the same request. This often happens when resolving nested fields from a list, for example (<a target="_blank" href="https://benoitboure.com/how-to-handle-many-to-many-relations-in-appsync">n+1 problem</a>). Be careful, the number of executions can compound pretty quickly, especially if you deep-nest resolvers (my friends, the friends of my friends, and their friends).</p>
<p>Still using VTL? The maximum size of VTL mapping templates is 64 kilobytes, while their maximum size after they are evaluated is 5 megabytes, and the number of iterations in a <code>foreach</code> loop is limited to 1,000.</p>
<p>If you're using <a target="_blank" href="https://blog.graphbolt.dev/everything-you-should-know-about-the-appsync-javascript-pipeline-resolvers">JavaScript resolvers</a>, you can't use more than 32,000 characters.</p>
<p>For pipeline resolvers, the maximum number of functions that can be used is 10 per pipeline. If you need more complex logic, it's probably time to switch to a Lambda resolver (<a target="_blank" href="https://twitter.com/radzikowski_m/status/1639646315419250689">or an Express Step Function</a>).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/Benoit_Boure/status/1639391102619856896">https://twitter.com/Benoit_Boure/status/1639391102619856896</a></div>
<p> </p>
<h2 id="heading-request-tokens">Request Tokens</h2>
<p>AWS AppSync allocates request tokens to mutation and query requests based on the amount of resources they consume, with a maximum rate of <s>2,000</s> up to 10,000 per second per region (depending on the region), shared by all the APIs!</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🆕</div>
<div data-node-type="callout-text">Update April 2, 2024 - AWS <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2024/04/aws-appsync-increases-service-quota-adds-subscription/">announced an increase of the limit</a> from 2,000 to up to 10,000 tokens per second per region, depending on the region</div>
</div>

<p>Requests consuming less than or equal to 1,500 KB-seconds of memory and vCPU time are allocated one token. After that, one more token is added per additional chunk of 1,500 KB-seconds consumed.</p>
<p>Although you can request a quota increase, a high token consumption could indicate the need to optimize requests and improve API performance. It also affects the number of requests that your APIs can receive per second. The more tokens they consume, the fewer concurrent requests they will accept.</p>
<h2 id="heading-subscriptions">Subscriptions</h2>
<p>The maximum size of a message that can be received through subscriptions is 240 kilobytes.</p>
<p>There is also a default limit of 100 subscriptions per WebSocket connection (that is, per client), but it can be adjusted.</p>
<p>Finally, a hard limit of 100 <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/aws-appsync-real-time-invalidation.html">subscription invalidation</a> requests per second applies.</p>
<p><strong>Update: April 2, 2024</strong></p>
<p>AWS added new adjustable quotas for subscriptions:</p>
<ul>
<li><p>maximum number of inbound subscription invocations (caused by a mutation invocation): 10,000 per second.</p>
</li>
<li><p>maximum number of outbound subscription messages delivered to clients (per 5kb payload): 1,000,000 per second.</p>
</li>
<li><p>maximum number of WebSocket connection requests: 2,000 per second</p>
</li>
</ul>
<h2 id="heading-other-limits">Other Limits</h2>
<p>Maximum size of the schema document: 1 megabyte.</p>
<p>The maximum number of AppSync custom domains that you can create in a region is 25, but you can request more if you need them.</p>
<p>If you're using <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/enabling-caching.html">caching</a>, you won't be able to use more than 25 caching keys per resolver, and each item has a maximum Time To Live (TTL) of 1 hour.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AWS AppSync offers a range of features and functionalities for building GraphQL APIs, but it's important to be aware of the service's limits. Remember that they are there for a reason; they are guard rails to prevent you from adopting bad practices that could affect the performance of your APIs and make sure they perform and scale.</p>
]]></content:encoded></item><item><title><![CDATA[Building Modern Serverless APIs with CDK, Python, and GraphQL (Part 5)]]></title><description><![CDATA[This is the 5th and final part of this series. 
In Part 1, we gave a brief overview of the concept of event-driven architectures, coupling, and defined all AWS services needed to build the API.
In Part 2, we created a new CDK project, and added CDK r...]]></description><link>https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-5</link><guid isPermaLink="true">https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-5</guid><category><![CDATA[serverless]]></category><category><![CDATA[aws-cdk]]></category><category><![CDATA[Python]]></category><category><![CDATA[graphbolt]]></category><dc:creator><![CDATA[Rosius Ndimofor]]></dc:creator><pubDate>Wed, 22 Mar 2023 12:13:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676823513697/fb2fe502-9362-4456-a6cc-79ddc010457b.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the 5th and final part of this series. </p>
<p>In <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-1">Part 1</a>, we gave a brief overview of the concept of event-driven architectures, coupling, and defined all AWS services needed to build the API.</p>
<p>In <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-2">Part 2</a>, we created a new CDK project, and added CDK resources for our GraphQL API, alongside a GraphQL Schema, an SQS queue, a DynamoDB table, and an SNS Topic.</p>
<p>In <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-3">Part 3</a>, we created resources and lambda functions for the step functions workflow.</p>
<p>In the fourth Part, we elaborated more on the step functions workflow, added SNS and deployed the code to the cloud.</p>
<h3 id="heading-api-testing-with-graphbolt">API TESTING WITH GRAPHBOLT</h3>
<p>When it comes to testing GraphQL APIs, there are a couple of options you can use. The very first one is the AWS AppSync Console. It comes loaded with stuff like</p>
<ul>
<li><p>Authentication</p>
</li>
<li><p>All Queries, Mutations, and Subscriptions</p>
</li>
<li><p>API Testing Interface</p>
</li>
<li><p>View and edit VTL templates, Functions, and Pipelines</p>
</li>
<li><p>And a lot more.</p>
</li>
</ul>
<p>Irrespective of all its pros, it has a couple of cons that slow down developer productivity and have you thinking about alternatives. For example</p>
<ul>
<li><p>Console Time out. I don't like signing into the console every 20 minutes of inactivity. This timeout can be increased to 60 minutes in the AWS settings. But security-wise, this isn't ideal. I don't need to expose my entire AWS Console because I'm testing an AppSync API.</p>
</li>
<li><p>Debugging your endpoint from AppSync is no fun. You need to open up the endpoint in Cloudwatch and search for the error through a hundred logs. This is one of the main reasons why observability platforms are on the rise every single day. Searching through Cloudwatch logs is stressful.</p>
</li>
</ul>
<p>There are API clients out there that make managing, testing, and debugging GraphQL APIs Fun.</p>
<p>Say Hi to Graphbolt.</p>
<h2 id="heading-graphbolt">GRAPHBOLT</h2>
<p>GraphBolt is a one-stop shop for everything related to AppSync to help you manage, build, test, and debug AppSync APIs.</p>
<p>The query client allows you to execute queries and mutations, just like with Postman or Insomnia. But it is Tailored for AppSync. Meaning, things like authentication (Cognito, API key, etc) are built-in and auto-detected. You only have to choose the right one. API keys are auto-detected and you only need to choose one vs to go copy and paste it in postman.</p>
<p>Let's take a quick look at its interface</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/graphbolt.png" alt="alt text" /></p>
<p>At the top of the image, GraphBolt displays the auto-detected AWS Profile you've configured on your computer. I've highlighted it with a yellow rectangle. You can click the dropdown to see all detected AWS profiles.</p>
<p>Top right-hand side, there's a lock 🔒 sign highlighted in pink. Used in configuring authentication.</p>
<p>On the left-hand side of the image, I've highlighted 3 icons with a blue rectangle. The first icon is the <strong>Query Client</strong>. It provides an interface for building and running Graphql endpoints.</p>
<p>The second icon is the <strong>Query Inspector</strong>. It provides an interface for debugging. The third icon is <strong>Mapping Template Designer</strong>. It provides an interface to build and evaluate VTL templates and javascript resolvers.</p>
<p>On the right-hand side of the image, I've highlighted the bug icon with a red rectangle. That icon also takes you to the query inspector screen, to debug the API endpoint you just ran.</p>
<p>Then we have the green, purple, and black rectangles.</p>
<p>The green rectangle is for building the query.</p>
<p>Purple is for providing variables to a query.</p>
<p>Black is for displaying the response to the request when ran. It contains the body and header of the response.</p>
<p>Clicking on the bug icon takes you to this screen. (Query Inspector)</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/debug.png" alt="alt text" /></p>
<p>It has the Resolvers, Query, Request Headers, Response Headers, and Raw logs for the GraphQl Query you just ran.</p>
<p>I've highlighted an eye icon on the right, we'll see its use later.</p>
<p>With GraphBolt, you don't need to open up CloudWatch in order to debug your API. Just execute a request. Seeing something wrong? Click the 🪲 button (top right-hand side) and you’re taken to the query inspector screen.</p>
<p>There, you can visualize all the resolvers that were executed. All errors will be highlighted in red.</p>
<p>By clicking on the 👁 of any resolver, you can dig in and instantly see the <code>$context</code> object, the compiled mapping template, and the response from the data source.</p>
<p>Finally, you will also see the result of the resolver (returned value).</p>
<p>This allows you to understand what is going on.</p>
<p>We'll definitely be using GraphBolt to test this and subsequent APIs.</p>
<p>So the first endpoint I want to test is the <code>postOrder</code> endpoint. I want to place an order and see if it gets executed successfully or not. I expect a message to be sent to SQS, the <code>postOrder</code> function should poll and start a step functions workflow, and the email should be sent to confirm if payment was successful or not.</p>
<p>Let's check to see if we have a valid API key first. Click on the lock icon</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/api_key.png" alt="alt text" /></p>
<p>The key is valid. If you don't have a valid key or no key at all, go to the settings menu in the AppSync console and create one.</p>
<p>GraphBolt would automatically detect the key once it has been created.</p>
<p>I'll make a request with this input. I want 6 cartons of pizza from Dominos Pizza.</p>
<pre><code class="lang-json">    name:<span class="hljs-string">"Pizza"</span>,
    quantity:<span class="hljs-number">6</span>,
    restaurantId:<span class="hljs-string">"Dominos Pizza"</span>
</code></pre>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/postOrder.png" alt="alt text" /></p>
<p>Then hit send.</p>
<p>Log into AWS Console and navigate to the <code>postOrder</code> lambda functions CloudWatch logs. You should see a log message from SQS. The <code>postOrder</code> function polled the message from SQS.</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/post_order_logs.png" alt="alt text" /></p>
<p>If you look in detail at the message, you would see the input we sent to AppSync in the body of the message.</p>
<pre><code class="lang-json">{
   <span class="hljs-attr">"Records"</span>:[
      {
         <span class="hljs-attr">"messageId"</span>:<span class="hljs-string">"80ddd9e2-740c-4c20-b6ce-c141382a21c8"</span>,
         <span class="hljs-attr">"receiptHandle"</span>:<span class="hljs-string">"AQEBzS2lYHfSFDM79iWKIYQ3tydLNRuZWs7BqQlRTXwXMf6lBjigjjOIpXSBds6f9qhgTvOIwWc7kA4OmauYPBWkgxNFdVmx6ktmO6ph5MrJTGmWHM2cAQ45TQeKQn1tBjtI/ilOFeKghLasFnsqNVTN+8phsdbUI1ZOFfMqeALk9B2gvYIroQDLpjZpUgZsCSwA0xIoF+9fyWPO24Yax3XkRT0AneYiy08Ckwy6/RR2M+o6uceC+4XjxlzMuV16yhuuxtQdIiE6gBh5v9wYJYp5haZHUZFd0jwJLgkjOMInytIO249X4/eLlHTUWL/iVUJ3OQt9J7EObHBCYrfH5ARXsyc/oPW7B4A/RNWgO2AAvKYlLVw0sDWpRIynbJml46B+nY59ho+wc8vn5jcAmOMFZA=="</span>,
         <span class="hljs-attr">"body"</span>:<span class="hljs-string">"{\"input\": {\"name\": \"Pizza\", \"quantity\": 6, \"restaurantId\": \"Dominos Pizza\"}}"</span>,
         <span class="hljs-attr">"attributes"</span>:{
            <span class="hljs-attr">"ApproximateReceiveCount"</span>:<span class="hljs-string">"1"</span>,
            <span class="hljs-attr">"SentTimestamp"</span>:<span class="hljs-string">"1676395715258"</span>,
            <span class="hljs-attr">"SenderId"</span>:<span class="hljs-string">"AROAR5S2TJZSTGYER2WGB:send-sqs-function"</span>,
            <span class="hljs-attr">"ApproximateFirstReceiveTimestamp"</span>:<span class="hljs-string">"1676395715261"</span>
         },
         <span class="hljs-attr">"messageAttributes"</span>:{

         },
         <span class="hljs-attr">"md5OfBody"</span>:<span class="hljs-string">"fe25d90fe8123ea670065bc94209c114"</span>,
         <span class="hljs-attr">"eventSource"</span>:<span class="hljs-string">"aws:sqs"</span>,
         <span class="hljs-attr">"eventSourceARN"</span>:<span class="hljs-string">"arn:aws:sqs:us-east-2:132260253285:sqs-queue"</span>,
         <span class="hljs-attr">"awsRegion"</span>:<span class="hljs-string">"us-east-2"</span>
      }
   ]
}
</code></pre>
<p>Next, open up the step functions workflow and see how the workflow played out.</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/step1.png" alt="alt text" /></p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/step2.png" alt="alt text" /></p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/step3.png" alt="alt text" /></p>
<p>From the workflow, we see that the payment was successful.</p>
<p>Now, we expect to receive a <code>SUCCESS</code> email</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/email.png" alt="alt text" /></p>
<p>If you keep hitting the send button in GraphBolt, you'll receive a mix of SUCCESS and FAILED emails.</p>
<p>Be sure to also open up DynamoDB and check on the saved data.</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/dynamo.png" alt="alt text" /></p>
<p>Before destroying or clearing up all created resources for your application. Yes, you have to destroy the application once you are done, in order to not rack up unnecessary AWS Bills.</p>
<p>Test other queries like <code>getAllOrders</code> or <code>getSingleOrder</code>.</p>
<h4 id="heading-get-all-orders">get all orders</h4>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/get_all_orders.png" alt="alt text" /></p>
<h4 id="heading-getsingleorder">getSingleOrder</h4>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/get_single_order.png" alt="alt text" /></p>
<h2 id="heading-what-next">What Next</h2>
<p>We've barely scratched the surface when it comes to building Event-Driven Applications. I urge you to take this app as a starting point and build out something close to production-grade. I left a lot of room for improvement in the app.</p>
<p>For example, in the step functions workflow, you can easily take out a couple of the lambda functions and create a direct service integration between step functions and DynamoDB.</p>
<p>Step functions support &gt;200 AWS Services.</p>
<p>Also, in the <code>complete_order</code> and <code>cancel_failed_order</code> lambda functions, we perform a DynamoDB update and then send an email through SNS.</p>
<blockquote>
<p>Writing to the database and sending the email aren’t in one transactional scope.</p>
</blockquote>
<p>What if the email fails to send when you've already done the DynamoDB update?</p>
<p>These are sweet edge cases you can resolve.</p>
<p>So instead of having lambda update DynamoDB as well as send an email, you can update DynamoDB directly in step functions, then use DynamoDB streams to connect to an SNS topic, through an event bridge pipe.</p>
<p>If you try this out, please do let me know how it goes.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial series, we looked at how to create an Event Driven Application using AWS Services. We built the api, using AppSync, Graphql and python, then tested with GraphBolt. We assumed you have a basic understanding of step functions and suggested a couple of articles where you could step up in case you've never heard of step functions before or you've heard, but never used it.</p>
<p>I really do hope you enjoyed this piece. If you did, do leave a comment or a like.</p>
<p>Happy Coding ✌🏾</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p>https://aws.amazon.com/blogs/compute/introducing-maximum-concurrency-of-aws-lambda-functions-when-using-amazon-sqs-as-an-event-source/</p>
</li>
<li><p>https://aws.amazon.com/blogs/compute/understanding-how-aws-lambda-scales-when-subscribed-to-amazon-sqs-queues/</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Building Modern Serverless APIs with CDK, Python, and GraphQL (Part 4)]]></title><description><![CDATA[Welcome to the fourth part of this article series. 
In Part 1, we gave a brief overview of the concept of event-driven architectures, coupling, and defined all AWS services needed to build the API.
In Part 2, we created a new CDK project, and added C...]]></description><link>https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-4</link><guid isPermaLink="true">https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-4</guid><category><![CDATA[AppSync]]></category><category><![CDATA[AWS]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Rosius Ndimofor]]></dc:creator><pubDate>Wed, 15 Mar 2023 08:55:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676823646038/cbaa8cc9-bd63-4923-92da-212495c171f3.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to the fourth part of this article series. </p>
<p>In <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-1">Part 1</a>, we gave a brief overview of the concept of event-driven architectures, coupling, and defined all AWS services needed to build the API.</p>
<p>In <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-2">Part 2</a>, we created a new CDK project, and added CDK resources for our GraphQL API, alongside a GraphQL Schema, an SQS queue, a DynamoDB table, and an SNS Topic.</p>
<p>In <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-3">Part 3</a>, we created resources and lambda functions for the step functions workflow.</p>
<p>Let's continue from where we left off in part 3.</p>
<p>From the GraphQL schema, the endpoint is under <code>Query</code></p>
<p><code>orders: [ Order ]!</code></p>
<p>We'll be using a Lambda function resolver to resolve this endpoint. Other alternatives are VTL resolvers or the newly added javascript resolvers. For now, Javascript resolvers are only compatible with applications built with Javascript (nodeJs or Typescript).</p>
<p>From my experience building GraphQL APIs, it's always better to use VTL resolvers for querying purposes. They are quicker, have zero cold starts, and are ideal in situations where there is less data manipulation.</p>
<p>But for this tutorial, we'll be querying the order data using a Lambda resolver. Sorry 😅</p>
<p>The first step towards creating our resolver is to first create a handler with the code to query DynamoDB.</p>
<p>Create a python package called <code>get_orders</code> within the <code>lambda-fns</code> folder. Create a python file called <code>get_order.py</code> within the <code>get_orders</code> folder.</p>
<p>Type in the following code to query all orders from the table.</p>
<pre><code class="lang-python">
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_response</span>(<span class="hljs-params">data</span>):</span>
    print(data)
    data[<span class="hljs-string">"id"</span>] = data[<span class="hljs-string">"id"</span>][<span class="hljs-string">"S"</span>]
    data[<span class="hljs-string">"quantity"</span>] = data[<span class="hljs-string">"quantity"</span>][<span class="hljs-string">"N"</span>]
    data[<span class="hljs-string">"name"</span>] = data[<span class="hljs-string">"name"</span>][<span class="hljs-string">"S"</span>]
    data[<span class="hljs-string">"restaurantId"</span>] = data[<span class="hljs-string">"restaurantId"</span>][<span class="hljs-string">"S"</span>]
    data[<span class="hljs-string">"orderStatus"</span>] = data[<span class="hljs-string">"orderStatus"</span>][<span class="hljs-string">"S"</span>]

    <span class="hljs-keyword">return</span> data


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_all_orders</span>(<span class="hljs-params">dynamo_client, table_name</span>):</span>
    results = []
    last_evaluated_key = <span class="hljs-literal">None</span>
    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        <span class="hljs-keyword">if</span> last_evaluated_key:
            response = ddb_client.scan(
                TableName=table_name, ExclusiveStartKey=last_evaluated_key
            )
        <span class="hljs-keyword">else</span>:
            response = dynamo_client.scan(TableName=table_name)

        last_evaluated_key = response.get(<span class="hljs-string">"LastEvaluatedKey"</span>)
        response = map(process_response, response[<span class="hljs-string">"Items"</span>])
        response = list(response)
        results.extend(response)

        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> last_evaluated_key:
            <span class="hljs-keyword">break</span>
    print(<span class="hljs-string">f"fetch_all_orders returned <span class="hljs-subst">{results}</span>"</span>)
    <span class="hljs-keyword">return</span> results


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handler</span>(<span class="hljs-params">event, context</span>):</span>
    items = fetch_all_orders(ddb_client, TABLE_NAME)
    <span class="hljs-keyword">return</span> items
</code></pre>
<p>We are using a <code>scan</code> and <code>LastEvaluatedKey</code> to paginate through the data in the database. This is not efficient and performant. As a matter of fact, don't do this. This is for tutorial purposes only.</p>
<p>In a real application, you'll want to make use of the <code>query</code> function and Global Secondary indexes (GSI) for better performance and scalability.</p>
<p>The next step is to define the lambda resources, the data source, and the lambda resolver.</p>
<p>I created a separate folder for files like this. So within the root folder, create a python package called <code>lambda_resources</code>. Within that folder, create a python file called <code>get_all_orders_lambda_resource.py</code>.</p>
<p>Type in the following code.</p>
<pre><code class="lang-python">
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_all_orders_lambda_resource</span>(<span class="hljs-params">stack, api, schema, db_role, lambda_execution_role</span>):</span>
    <span class="hljs-keyword">with</span> open(<span class="hljs-string">"lambda_fns/get_orders/get_orders.py"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> file:
        get_all_order_lambda_function = file.read()

    get_all_order_function = lambda_.CfnFunction(
        stack,
        <span class="hljs-string">"gets"</span>,
        code=lambda_.CfnFunction.CodeProperty(zip_file=get_all_order_lambda_function),
        role=db_role.role_arn,
        <span class="hljs-comment"># the properties below are optional</span>
        architectures=[<span class="hljs-string">"x86_64"</span>],
        description=<span class="hljs-string">"lambda-ds"</span>,
        environment=lambda_.CfnFunction.EnvironmentProperty(
            variables={<span class="hljs-string">"ORDER_TABLE"</span>: <span class="hljs-string">"ORDER"</span>}
        ),
        function_name=<span class="hljs-string">"get-orders-function"</span>,
        handler=<span class="hljs-string">"index.handler"</span>,
        package_type=<span class="hljs-string">"Zip"</span>,
        runtime=<span class="hljs-string">"python3.9"</span>,
        timeout=<span class="hljs-number">123</span>,
        tracing_config=lambda_.CfnFunction.TracingConfigProperty(mode=<span class="hljs-string">"Active"</span>),
    )
</code></pre>
<p>We've seen similar code like this above. So we'll go ahead and create a new AppSync data source and then attach our lambda to it.</p>
<p>Within the same file, type in this code</p>
<pre><code class="lang-python"> lambda_get_all_order_config_property = appsync.CfnDataSource.LambdaConfigProperty(
        lambda_function_arn=get_all_order_function.attr_arn
    )

    lambda_get_all_order_data_source = appsync.CfnDataSource(
        scope=stack,
        id=<span class="hljs-string">"lambda-getAll-order-ds"</span>,
        api_id=api.attr_api_id,
        name=<span class="hljs-string">"lambda_getAll_order_ds"</span>,
        type=<span class="hljs-string">"AWS_LAMBDA"</span>,
        lambda_config=lambda_get_all_order_config_property,
        service_role_arn=lambda_execution_role.role_arn,
    )
</code></pre>
<p>Using the <code>LambdaConfigProperty</code> we specify the lambda functions ARN for AppSync's Datasource. Then in the <code>CfnDataSource</code> method, we specify the AppSync's API id, the type of Datasource (AWS_LAMBDA) the name of the Datasource, the lambda configuration, and the AWS IAM role ARN for the Datasource.</p>
<p>The next step is defining the resolver.</p>
<pre><code class="lang-python">    <span class="hljs-comment"># Resolvers</span>

    <span class="hljs-comment">## list orders resolver</span>
    get_all_orders_resolver = appsync.CfnResolver(
        stack,
        <span class="hljs-string">"list-orders"</span>,
        api_id=api.attr_api_id,
        field_name=<span class="hljs-string">"orders"</span>,
        type_name=<span class="hljs-string">"Query"</span>,
        data_source_name=lambda_get_all_order_data_source.name,
    )
</code></pre>
<p>In the above code, using the <code>CfnResolver</code> method from AppSync, we add in the AppSync API id, the field_name and type_name as specified in the GraphQL schema, and also the name of the Datasource we created above.</p>
<p>Lastly, since the resolver depends on the GraphQL schema and Datasource, we need to specify that also, using the <code>add_dependency</code> method.</p>
<pre><code class="lang-python">    get_all_orders_resolver.add_dependency(schema)
    get_all_orders_resolver.add_dependency(lambda_get_all_order_data_source)
</code></pre>
<p>We are just adding a CloudFormation dependency between both resources.</p>
<p>The final step is to call this function <code>get_all_orders_lambda_resource(stack, api, schema, db_role, lambda_execution_role)</code> from the stack file and pass in all required parameters.</p>
<h2 id="heading-deploy">Deploy</h2>
<p>I'll assume you've already added your account and region as environment variables to your app file in <code>app.py</code>.</p>
<p>Run</p>
<p><code>cdk synth</code> in order to synthesize your app.</p>
<p>Then run</p>
<p><code>cdk bootstrap</code> to provision CloudFormation resources to the environment (region and account) we added to the app above.</p>
<p>Finally, we have to deploy the app. Remember that we need to pass in an email address as a parameter during deployment so that it can be used to subscribe to SNS for emails.</p>
<p>Run this command to deploy your app</p>
<p><code>cdk deploy --parameter subscriptionEmail=YOUR_EMAIL_ADDRESS</code></p>
<p>If the app is successfully deployed, you should receive a subscription email from amazon. Check your spam folder.</p>
<p>Clicking on the link in the email subscribes that email to the SNS topic we created above <code>sns topic</code>.</p>
<p>If you log into the AWS console and navigate to SNS, under your topic, you should see all subscribed emails.</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/sns_topic.png" alt="alt text" /></p>
<p>Subscribers</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/sns-sub.png" alt="alt text" /></p>
<p>Navigate to AppSync from the AWS console. Select and open your project from the list of APIs.</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/appsync_api.png" alt="alt text" /></p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/settings.png" alt="alt text" /></p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/valid_api.png" alt="alt text" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, we added an endpoint to get all orders in the database. We saw how to build the lambda function, create a Datasource and a resolver, then crowned it all up by deploying the app to the cloud, while passing in an email address as a parameter.</p>
<p>So what's left now is testing, and we'll be seeing that in the final episode of this series. Stay tuned. If you loved this piece, please like or comment. Happy Coding ✌🏾</p>
]]></content:encoded></item><item><title><![CDATA[Building Modern Serverless APIs with CDK, Python, and GraphQL (Part 3)]]></title><description><![CDATA[Welcome to the third part of this article series.
In Part 1, we gave a brief overview of the concept of event-driven architectures, coupling, and defined all AWS services needed to build the API.
In Part 2, we created a new CDK project, and added CDK...]]></description><link>https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-3</link><guid isPermaLink="true">https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-3</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[AWS]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Rosius Ndimofor]]></dc:creator><pubDate>Wed, 08 Mar 2023 08:00:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676823666643/5edd1c13-dc9f-4a07-a6a0-2deb2dfa0a25.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to the third part of this article series.</p>
<p>In <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-1">Part 1</a>, we gave a brief overview of the concept of event-driven architectures, coupling, and defined all AWS services needed to build the API.</p>
<p>In <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-2">Part 2</a>, we created a new CDK project, and added CDK resources for our GraphQL API, alongside a GraphQL Schema, an SQS queue, a DynamoDB table, and an SNS Topic.</p>
<p>Let's proceed to build from where we left off in part 2.</p>
<h3 id="heading-aws-step-functions-resources">AWS Step Functions Resources</h3>
<p>We won't be looking at how to create a step functions workflow from scratch. I'll assume you at least have a basic understanding of the service.</p>
<p>If you don't, don't worry. I've written a couple of articles to get you up and running in no time.</p>
<ul>
<li><p><a target="_blank" href="https://phatrabbitapps.com/building-apps-with-step-functions">Building Apps with Step Functions</a></p>
</li>
<li><p><a target="_blank" href="https://phatrabbitapps.com/building-a-step-functions-workflow-with-cdk-appsync-and-python">Building a Step Functions Workflow With CDK, AppSync, and Python</a></p>
</li>
</ul>
<p>The Step Functions workflow has 4 Lambda functions. Here's what it looks like visually. I exported this image from the Step Functions Visual Workflow.</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/stepfunctions_graph.png" alt="alt text" /></p>
<p>I also exported the workflow as a JSON file and saved it inside a python package called <code>step_function_workflow</code> in the project directory.</p>
<p>You can create a python package with the same name and get the file here. <a target="_blank" href="https://github.com/trey-rosius/event_driven_cdk/blob/master/step_function_workflow/workflow.json">workflow.json</a></p>
<p>For the 4 Lambda functions, navigate to the <code>lambda_fns</code> folder, create 4 python packages, and within each package, create a python file. These python files would serve as lambda functions.</p>
<ol>
<li><p>Package name: <code>initialize_order</code>, File name: <code>initialize_order.py</code></p>
</li>
<li><p>Package name: <code>process_payment</code>, File name: <code>process_payment.py</code></p>
</li>
<li><p>Package name: <code>complete_order</code>, File name: <code>complete_order.py</code></p>
</li>
<li><p>Package name: <code>cancel_failed_order</code>, File name: <code>cancel_failed_order.py</code></p>
</li>
</ol>
<p>Create a file called <code>step_function.py</code> within the <code>step_function_workflow</code> python package. We'll define all the lambda functions resources needed by the workflow within this file.</p>
<p>We have to create a Lambda resource for each of the above 4 python files, add them to the step functions workflow and then return a step functions instance.</p>
<p>In order to create a Lambda function resource, we need to first import the <code>aws_lambda</code> class from CDK.</p>
<p><code>from aws_cdk import aws_lambda as lambda_</code></p>
<p>Create a function with these parameters. We'll be passing in the stack, lambda permissions, and an SNS topic we created in the stack file.</p>
<p><code>def create_step_function(stack, lambda_step_function_role, cfn_topic):</code></p>
<p>Next, using the function <code>CfnFunction</code> found within the <code>aws_lambda</code> class, let's create the lambda function resource for <code>initialize_order.py</code>.</p>
<p>Firstly, we need to return a stream of the Lambda function, using the <code>open</code> method. We'll repeat these same steps for the other lambda functions.</p>
<pre><code class="lang-python"><span class="hljs-keyword">with</span> open(<span class="hljs-string">"lambda_fns/initialize_order/initialize_order.py"</span>, <span class="hljs-string">'r'</span>) <span class="hljs-keyword">as</span> file:
    initialize_order = file.read()
</code></pre>
<p>Then, we define the resource the Lambda function needs</p>
<pre><code class="lang-python"> initialize_order_function = \
        lambda_.CfnFunction(stack, <span class="hljs-string">"initialize-order-function"</span>,
        code=lambda_.CfnFunction.CodeProperty(
            zip_file=initialize_order
        ),
        role=lambda_step_function_role.role_arn,

        <span class="hljs-comment"># the properties below are optional</span>
        architectures=[<span class="hljs-string">"x86_64"</span>],
        description=<span class="hljs-string">"lambda-ds"</span>,
        environment=lambda_.CfnFunction.EnvironmentProperty(
            variables={
                <span class="hljs-string">"ORDER_TABLE"</span>: <span class="hljs-string">"ORDER"</span>,
                <span class="hljs-string">"TOPIC_ARN"</span>: cfn_topic.attr_topic_arn
            }
        ),
        function_name=<span class="hljs-string">"initialize-order-function"</span>,
        handler=<span class="hljs-string">"index.handler"</span>,
        package_type=<span class="hljs-string">"Zip"</span>,
        runtime=<span class="hljs-string">"python3.9"</span>,
        timeout=<span class="hljs-number">123</span>,
        tracing_config=lambda_.CfnFunction.TracingConfigProperty(
            mode=<span class="hljs-string">"Active"</span>
        )
        )
</code></pre>
<p>Here's what's happening in the above code.</p>
<p>To create a Lambda function, we need a deployment package and an execution role. Our deployment package in this case is a zip file, containing the <code>initialize_order</code> Lambda code. You can also use a container image as your deployment package.</p>
<pre><code class="lang-python">code=lambda_.CfnFunction.CodeProperty(zip_file=initialize_order),
</code></pre>
<p>We also need an execution role that grants the function permission to access other AWS services such as Step Functions, CloudWatch, etc.</p>
<p><code>role=lambda_step_function_role.role_arn,</code></p>
<p>Here are the policies we attached to the lambda function role in the stack file.</p>
<ul>
<li><p>Full DynamoDB access (Because we'll be saving the order to the database)</p>
</li>
<li><p>Full SNS access for sending emails</p>
</li>
<li><p>Full CloudWatch access for logging</p>
</li>
</ul>
<pre><code class="lang-python">lambda_step_function_role = \
            iam.Role(self, <span class="hljs-string">"LambdaStepFunctionRole"</span>,
             assumed_by=iam.ServicePrincipal(<span class="hljs-string">"lambda.amazonaws.com"</span>),
             managed_policies=[db_full_access_role,
                               cloud_watch_role_full_access,
                               sns_policy])
</code></pre>
<p>Then we set the environment variables that we'll be needing in our Lambda function. In this case, it's the DynamoDB table name and SNS topic Amazon resource name (ARN).</p>
<p>Next, we state the runtime (python 3.9), handler name (which we'll define later within the <code>initialize_order.py</code> file, and the Lambda function timeout in seconds. Note that a lambda function can't run above 15 minutes.</p>
<p>Repeat these same steps for the other 3 lambda functions.</p>
<p>Use the file on Github as a reference: <a target="_blank" href="https://github.com/trey-rosius/event_driven_cdk/blob/master/step_function_workflow/step_function.py">step_function.py</a></p>
<p>At the bottom of the <code>step_function.py</code> file, we have to define the Step Functions workflow template and also add the created Lambda functions ARNs.</p>
<pre><code class="lang-python">    sf_workflow = \
        Template(workflow).substitute(InitializeOrderArn=initialize_order_function.attr_arn,
        ProcessPaymentArn=process_payment_function.attr_arn,
        CompleteOrderArn=complete_order_function.attr_arn,
        CancelFailedOrderArn=cancel_failed_order_function.attr_arn, dollar=<span class="hljs-string">"$"</span>)

    <span class="hljs-keyword">return</span> sf_workflow
</code></pre>
<p>Inside the <code>workflow.json</code> file, there's a line like this</p>
<p><code>"FunctionName": "$InitializeOrderArn"</code></p>
<p>We use the <code>substitute</code> method above to replace that line with the ARN to where the Lambda function resides <code>InitializeOrderArn=initialize_order_function.attr_arn</code>.</p>
<p>Now, go to the stack file of your application, and type in the following code to create a Step Functions state machine.</p>
<p>Firstly, we need to call the <code>create_step_function(self, lambda_step_function_role, cfn_topic)</code> function we defined above and pass its return type to our state machine's definition.</p>
<p>Also, the state machine needs permission to interact with other AWS services. So we grant Step Functions permissions to execute lambda functions.</p>
<pre><code class="lang-python"> workflow = create_step_function(self, lambda_step_function_role, cfn_topic)

        simple_state_machine = \
            stepfunctions.CfnStateMachine(self, <span class="hljs-string">"SimpleStateMachine"</span>,
             definition=json.loads(workflow),
             role_arn=lambda_execution_role.role_arn
             )
</code></pre>
<p>At this point, we've created resources for 75% of the application. Let's go ahead and start creating the endpoints to consume these resources.</p>
<h3 id="heading-endpoints">Endpoints</h3>
<h4 id="heading-create-order">Create Order</h4>
<p>When a user places an order, a series of events happen</p>
<ul>
<li><p>An AppSync Mutation is invoked. The input to the Mutation is bundled and sent as a message to an SQS Queue.</p>
</li>
<li><p>A Lambda function (post order Lambda) polls the messages from the SQS Queue, extracts the order information, and starts up a Step Functions workflow with order information as input.</p>
</li>
<li><p>The Step Functions workflow contains 4 different Lambda functions (Initialize Order, Process Payment, Complete Order, Cancel Order).</p>
</li>
<li><p>The Initialize Order function creates a new order in the database.</p>
</li>
<li><p>The Process Payment function randomly assigns a <code>success</code> or <code>failed</code> payment status to the order.</p>
</li>
<li><p>If payment was successful, the Complete Order function updates the order in the database and sends an email to the client.</p>
</li>
<li><p>If payment failed, the order status is updated in the database and an email is sent with failed status to the client through the failed email Lambda function.</p>
</li>
</ul>
<p>Create a python package inside the <code>lambda-fns</code> folder called <code>post_order</code>. Then inside that folder, create a python file called <code>post_order.py</code>.</p>
<p>This Lambda function would poll messages from the SQS queue and start a Step Functions workflow. Therefore, it'll need permission to receive SQS queue messages and to start a Step Functions workflow.</p>
<p>Let's define the code within the <code>post_order.py</code> file.</p>
<pre><code class="lang-python">
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handler</span>(<span class="hljs-params">event, context</span>):</span>
    new_order_list = []
    print(<span class="hljs-string">"event "</span>, event)
    <span class="hljs-keyword">for</span> record <span class="hljs-keyword">in</span> event[<span class="hljs-string">"Records"</span>]:
        message_id = record[<span class="hljs-string">"messageId"</span>]
        request_body = json.loads(record[<span class="hljs-string">"body"</span>])
        order_data = request_body[<span class="hljs-string">"input"</span>]
        print(<span class="hljs-string">f'post_orders reqeust_body <span class="hljs-subst">{order_data}</span> type: <span class="hljs-subst">{type(order_data)}</span>'</span>)
        sfn_input = assemble_order(message_id, order_data)
        response = start_sfn_exec(sfn_input, message_id)
        print(<span class="hljs-string">f'start sfn execution: <span class="hljs-subst">{response}</span>'</span>)
        new_order_list.append(response[<span class="hljs-string">"executionArn"</span>])
    <span class="hljs-keyword">return</span> new_order_list
</code></pre>
<p>Messages from a queue are polled as <code>Records</code>. We have to iterate over each record in order to extract the order information, assemble the order, and use it to start a step functions workflow.</p>
<p><code>sfn_input = assemble_order(message_id, order_data)</code></p>
<p>This method simply adds more random data to the created order and returns the order.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">assemble_order</span>(<span class="hljs-params">message_id, order_data</span>):</span>
    now = datetime.now()
    order_data[<span class="hljs-string">"user_id"</span>] = <span class="hljs-string">"demo_user"</span>
    order_data[<span class="hljs-string">"id"</span>] = message_id
    order_data[<span class="hljs-string">"orderStatus"</span>] = DEFAULT_ORDER_STATUS
    order_data[<span class="hljs-string">"createdAt"</span>] = now.isoformat()
    <span class="hljs-keyword">return</span> json.dumps(order_data, cls=DecimalEncoder)
</code></pre>
<p><code>response = start_sfn_exec(sfn_input, message_id)</code></p>
<p>Here's where we start the step functions workflow, using the Step Function ARN and the order data as input.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">start_sfn_exec</span>(<span class="hljs-params">sfn_input, sfn_exec_id</span>):</span>
    response = sfn.start_execution(
        stateMachineArn=STATE_MACHINE_ARN,
        name=sfn_exec_id,
        input=json.dumps(sfn_input, cls=DecimalEncoder)
    )
    print(<span class="hljs-string">f'post_orders start sfn_exec_id <span class="hljs-subst">{sfn_exec_id}</span> and input <span class="hljs-subst">{sfn_input}</span>'</span>)
    <span class="hljs-keyword">return</span> response
</code></pre>
<p>Now, define the resources and permissions this Lambda function needs. It's pretty similar to the Step Functions lambda resources we defined above.</p>
<p>Within the stack folder, mine is <code>event_driven_cdk_app</code> create a file called <code>post_order_resource.py</code> and type in the following code.</p>
<pre><code class="lang-python">
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_post_order_lambda_resource</span>(<span class="hljs-params">stack, simple_state_machine, sqs_receive_message_role, queue</span>):</span>
    <span class="hljs-keyword">with</span> open(<span class="hljs-string">"lambda_fns/post_order/post_order.py"</span>, <span class="hljs-string">'r'</span>) <span class="hljs-keyword">as</span> file:
        post_function_file = file.read()

    post_function = lambda_.CfnFunction(stack, <span class="hljs-string">"post"</span>,
                    code=lambda_.CfnFunction.CodeProperty(
                        zip_file=post_function_file
                    ),
                    role=sqs_receive_message_role.role_arn,

                    <span class="hljs-comment"># the properties below are optional</span>
                    architectures=[<span class="hljs-string">"x86_64"</span>],
                    description=<span class="hljs-string">"lambda-ds"</span>,
                    environment=lambda_.CfnFunction.EnvironmentProperty(
                        variables={
                            <span class="hljs-string">"ORDER_TABLE"</span>: <span class="hljs-string">"ORDER"</span>,
                            <span class="hljs-string">"STATE_MACHINE_ARN"</span>: simple_state_machine.attr_arn
                        }
                    ),
                    function_name=<span class="hljs-string">"post-order-function"</span>,
                    handler=<span class="hljs-string">"index.handler"</span>,
                    package_type=<span class="hljs-string">"Zip"</span>,
                    runtime=<span class="hljs-string">"python3.9"</span>,
                    timeout=<span class="hljs-number">123</span>,
                    tracing_config=lambda_.CfnFunction.TracingConfigProperty(
                        mode=<span class="hljs-string">"Active"</span>
                    )
                    )

    lambda_.EventSourceMapping(scope=stack, id=<span class="hljs-string">"MyEventSourceMapping"</span>,
           target=post_function,
           batch_size=<span class="hljs-number">5</span>,
           enabled=<span class="hljs-literal">True</span>,
           event_source_arn=queue.attr_arn)
</code></pre>
<p>It's very identical to the one we defined above, except for the last part</p>
<pre><code class="lang-python">    lambda_.EventSourceMapping(scope=stack, id=<span class="hljs-string">"MyEventSourceMapping"</span>,
                               target=post_function,
                               batch_size=<span class="hljs-number">5</span>,
                               enabled=<span class="hljs-literal">True</span>,
                               event_source_arn=queue.attr_arn)
</code></pre>
<p>Remember, this function is responsible for polling messages from an SQS queue. We subscribe to the SQS queue using the <code>EventSourceMapping</code> function of the lambda API by passing in the SQS queue's ARN as the event source <code>event_source_arn=queue.attr_arn</code>.</p>
<p>The <code>batch_size</code> indicates the number of messages to be polled from SQS at a time.</p>
<p>When this function invokes a Step Function workflow, the first Lambda function to run in the workflow is <code>initialize_order.py</code>.</p>
<p>We created the file in the <code>initialize_order</code> python package, but we didn't add any code to it. Open up <code>initialize_order.py</code> and type in the following code.</p>
<p>All we do is save the order to the database and forward the order details to the next function, which is <code>process_payment.py</code></p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">persist_order</span>(<span class="hljs-params">order_item</span>):</span>
    print(<span class="hljs-string">f'persist_order item <span class="hljs-subst">{order_item}</span> to table <span class="hljs-subst">{TABLE_NAME}</span>'</span>)
    response = table.put_item(Item=order_item)
    message = {<span class="hljs-string">"order_status"</span>: order_item[<span class="hljs-string">"orderStatus"</span>], <span class="hljs-string">"order_id"</span>: order_item[<span class="hljs-string">"id"</span>]}
    print(<span class="hljs-string">f'new order pending payment <span class="hljs-subst">{message}</span>'</span>)
    <span class="hljs-keyword">return</span> {

        <span class="hljs-string">"message"</span>: json.dumps(order_item, indent=<span class="hljs-number">4</span>, cls=DecimalEncoder)
    }
</code></pre>
<p>You can grab the complete code <a target="_blank" href="https://github.com/trey-rosius/event_driven_cdk/blob/master/lambda_fns/process_payment/process_payment.py">here</a></p>
<p>The <code>process_payment.py</code> function randomly assigns a payment state (<code>ok</code> or <code>error</code>) and an error message if the randomly assigned state was <code>error</code>, and then moves to the next function.</p>
<p>The next function is determined by a choice state based on the state of the payment. The next function would be <code>complete_order.py</code> if the payment state was <code>ok</code> or <code>cancel_failed_order.py</code> if the payment state was <code>error</code>.</p>
<p>In either of these functions, the order is updated in the database, and an email is sent using SNS.</p>
<p>For the complete order function, the order is updated like so</p>
<pre><code class="lang-python">    response = table.update_item(
        Key={
            <span class="hljs-string">"user_id"</span>: event[<span class="hljs-string">"saveResults"</span>][<span class="hljs-string">"user_id"</span>],
            <span class="hljs-string">"id"</span>: event[<span class="hljs-string">"saveResults"</span>][<span class="hljs-string">"id"</span>]
        },
        UpdateExpression=<span class="hljs-string">"set orderStatus = :s"</span>,
        ExpressionAttributeValues={
            <span class="hljs-string">":s"</span>: order_status
        },
        ReturnValues=<span class="hljs-string">"UPDATED_NEW"</span>
    )
</code></pre>
<p>And an email is sent as such</p>
<pre><code class="lang-python">
  sns.publish(
        TopicArn=topic_arn,
        Message=json.dumps(message),
        Subject=<span class="hljs-string">f'Orders-App: Update for order <span class="hljs-subst">{message[<span class="hljs-string">"order_id"</span>]}</span>'</span>

    )
</code></pre>
<p>See the complete code here <a target="_blank" href="https://github.com/trey-rosius/event_driven_cdk/blob/master/lambda_fns/complete_order/complete_order.py">complete_order</a></p>
<p>Same thing with <code>cancel_failed_order.py</code> function <a target="_blank" href="https://github.com/trey-rosius/event_driven_cdk/blob/master/lambda_fns/cancel_failed_order/cancel_failed_order.py">Cancel Failed Order</a></p>
<p>At this point, we could deploy and test the API. But I think it'll be better to add at least one more endpoint before deploying.</p>
<p>I would love to see a list of all the orders made.</p>
<p>But that'll be for the next article.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this post, we added a Step functions workflow containing 4 Lambda functions to our API. We saw how to initialize an order and also send SNS email notifications of the status of an order. We'll continue building the API in the next post.</p>
<p>I'll love to know your thoughts about the series so far. Please leave a like or comment.</p>
<p>And until next time folks ✌🏾</p>
]]></content:encoded></item><item><title><![CDATA[Write Reusable Code for AppSync JavaScript Resolvers]]></title><description><![CDATA[Introduction
AWS AppSync is a fully managed service that allows developers to build scalable and performant GraphQL APIs. It is also serverless, meaning that you will only pay for what you use.
AWS recently introduced support for JavaScript resolvers...]]></description><link>https://blog.graphbolt.dev/write-reusable-code-for-appsync-javascript-resolvers</link><guid isPermaLink="true">https://blog.graphbolt.dev/write-reusable-code-for-appsync-javascript-resolvers</guid><category><![CDATA[AppSync]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[#graphql-tools]]></category><category><![CDATA[serverless]]></category><category><![CDATA[aws appsync]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Mon, 06 Mar 2023 06:12:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678535953722/9a522e3f-6f43-4085-b5d2-7365ab5fc13d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>AWS AppSync is a fully managed service that allows developers to build scalable and performant GraphQL APIs. It is also serverless, meaning that you will only pay for what you use.</p>
<p>AWS recently introduced <a target="_blank" href="https://aws.amazon.com/blogs/aws/aws-appsync-graphql-apis-supports-javascript-resolvers/">support for JavaScript resolvers</a>. This was a long-awaited feature. Before that, developers had to learn and write resolvers in a language called VTL, which has a steep learning curve and is hard to debug.</p>
<p>Shortly after the announcement, I wrote an article explaining <a target="_blank" href="https://blog.graphbolt.dev/everything-you-should-know-about-the-appsync-javascript-pipeline-resolvers">what JS resolvers are,</a> and why they are beneficial, but also explaining the limitations they have. For example, you likely cannot use your favorite external libraries. For two main reasons:</p>
<ol>
<li><p><code>import</code> (or <code>require</code>) is not allowed. All your code must be in the same file.</p>
</li>
<li><p>The code must comply with all the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-util-reference-js.html">strict rules</a> that AppSync has, which is likely not the case for most packages on npm today.</p>
</li>
</ol>
<p>But that does not mean you cannot write your own reusable code that can be shared between resolvers. As developers, we often try to be <a target="_blank" href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a>. If some code has to be written more than once, then it should be encapsulated into a function.</p>
<p>Right after JavaScript resolvers were announced, I did a quick proof of concept that shows that it is possible; as long as you follow the rules.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/Benoit_Boure/status/1594686086000828423">https://twitter.com/Benoit_Boure/status/1594686086000828423</a></div>
<p> </p>
<p>In this article, I will show you how I achieved it with a simple practical example.</p>
<h1 id="heading-pre-requisites-and-assumptions">Pre-requisites and Assumptions</h1>
<p>In order to keep this article focused on the subject and take it to the point, I will take a few shortcuts and assumptions.</p>
<ol>
<li><p>You are already familiar with JavaScript resolvers.</p>
<p> I will not go into details about how JS resolvers work. If you are new to JS resolvers, please refer to <a target="_blank" href="https://aws.amazon.com/blogs/aws/aws-appsync-graphql-apis-supports-javascript-resolvers/">this article</a> and the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html">documentation</a> before you keep reading.</p>
</li>
<li><p>Pure JavaScript.</p>
<p> Even though we will use <a target="_blank" href="https://esbuild.github.io/">esbuild</a> (more on that later), for the purpose of this article, I will not try to use TypeScript and stick to pure JavaScript. This will remove the extra complexity that TypeScript adds because of code transpiling. In a follow-up article, we will see how we can add TypeScript into the mix.</p>
</li>
<li><p>Take IaC out of the equation.</p>
<p> I will not show you how to couple this with any Infrastructure as Code (IaC), nor how to deploy the resulting resolvers either. At the time of writing this article, and as far as I know, no IaC solution out there today supports this out of the box. You will need to figure out that part yourself. However, I will show you how to test the result, so you can see if the code is valid and working as expected 🙂</p>
</li>
</ol>
<h1 id="heading-lets-get-started">Let's get started</h1>
<p>Enough talk, let's write some code!</p>
<p>Imagine we want to add timestamp values (<code>createdAt</code> and <code>updatedAt</code>) for every item inserted into DynamoDB by all of our Mutations (e.g. <code>createPost</code>, <code>createAuthor</code>, <code>createComment</code>, etc.). Because we don't want to repeat that code in every single resolver, we might want to do something like this:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// createPost.js</span>
<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">import</span> { createItem } <span class="hljs-keyword">from</span> <span class="hljs-string">'./helpers'</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-comment">// add timestamps</span>
  <span class="hljs-keyword">const</span> item = createItem(ctx.args.post);

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">operation</span>: <span class="hljs-string">'PutItem'</span>,
    <span class="hljs-attr">key</span>: {
      <span class="hljs-attr">id</span>: util.dynamodb.toDynamoDB(util.autoId()),
    },
    <span class="hljs-attr">attributeValues</span>: util.dynamodb.toMapValues(item),
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span>(<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">return</span> ctx.result;
}
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">// helpers.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createItem</span>(<span class="hljs-params">item</span>) </span>{
  <span class="hljs-keyword">return</span> {
    ...item,
    <span class="hljs-attr">createdAt</span>: util.time.nowISO8601(),
    <span class="hljs-attr">updatedAt</span>: util.time.nowISO8601(),
  };
}
</code></pre>
<p>As you can see, we created a helper function named <code>createItem()</code> in a new file (<code>helpers.js</code>) that adds the creation and update timestamps to the object that is passed as an argument and then returns it. We import that function into our resolver and use it. We can do so in every place where we need it (e.g. <code>createAuthor.js</code>, <code>createComment.js</code>, etc).</p>
<p>This is great, but it also breaks one of the AppSync JS rules: <code>import</code> is not allowed. (Note: with the exception of <code>@aws-appsync/utils</code>, which is a special package provided by AppSync).</p>
<p>However, what we can do is bundle our code into one single file before sending it to AppSync. To do so, we are going to leverage a widely used code bundler: esbuild.</p>
<p>There are a few things to keep in mind though:</p>
<ol>
<li><p>By default, esbuild converts ECMAScript module syntax to CommonJS.</p>
<p> AppSync only supports ES modules, so we must ensure that esbuild outputs those instead. To solve that issue, we can use the <code>format</code> option with a value of <code>esm</code>.</p>
</li>
<li><p>We don't want to bundle the AppSync utils library.</p>
<p> AppSync provides the <code>@aws-appsync/utils</code> package by default. So, we don't want to have it bundled into our final code. The <code>external</code> option allows us to exclude it from the bundle.</p>
</li>
</ol>
<p>Let's execute the following command:</p>
<pre><code class="lang-bash">esbuild createPost.js \
  --bundle \
  --outdir=build \
  --external:<span class="hljs-string">"@aws-appsync/utils"</span> \
  --format=esm
</code></pre>
<p>If you look in the <code>build</code> directory, you should find a <code>createPost.js</code> file which contains the bundled code as follows:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// createPost.js</span>
<span class="hljs-keyword">import</span> { util <span class="hljs-keyword">as</span> util2 } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-appsync/utils"</span>;

<span class="hljs-comment">// helpers.js</span>
<span class="hljs-keyword">import</span> { util } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-appsync/utils"</span>;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createItem</span>(<span class="hljs-params">item</span>) </span>{
  <span class="hljs-keyword">return</span> {
    ...item,
    <span class="hljs-attr">createdAt</span>: util.time.nowISO8601(),
    <span class="hljs-attr">updatedAt</span>: util.time.nowISO8601()
  };
}

<span class="hljs-comment">// createPost.js</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> item = createItem(ctx.args.post);
  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">operation</span>: <span class="hljs-string">"PutItem"</span>,
    <span class="hljs-attr">key</span>: {
      <span class="hljs-attr">id</span>: util2.dynamodb.toDynamoDB(util2.autoId())
    },
    <span class="hljs-attr">attributeValues</span>: util2.dynamodb.toMapValues(item)
  };
}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span>(<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">return</span> ctx.result;
}
<span class="hljs-keyword">export</span> {
  request,
  response
};
</code></pre>
<p>We now have all the necessary code for our resolver in one single file that is compliant with AppSync 🎉</p>
<h1 id="heading-testing">Testing</h1>
<p>Great, now there is one more thing that we need to make sure of. Does our resolver work as expected? Does it contain any invalid code?</p>
<p>AWS provides a <a target="_blank" href="https://aws.amazon.com/cli/">CLI</a> that allows us to <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/test-debug-resolvers-js.html">test JavaScript resolvers</a>. It serves two purposes:</p>
<ul>
<li><p>If the code contains any error or is invalid, the command will fail and let us know where the problem is. This is especially useful in this case because esbuild might easily break compatibility with AppSync when bundling the code.</p>
</li>
<li><p>We can inject mock data in order to check the result that would be sent to the data source. This allows us to check that our code logic works as expected.</p>
</li>
</ul>
<p>Let's try it</p>
<pre><code class="lang-bash">aws appsync evaluate-code \
  --code file://build/createPost.js \
  --<span class="hljs-keyword">function</span> request \
  --context file://context.json \
  --runtime name=APPSYNC_JS,runtimeVersion=1.0.0
</code></pre>
<p>with the following <code>context.json</code> file.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"arguments"</span>: {
    <span class="hljs-attr">"post"</span>: {
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Hello, world"</span>,
      <span class="hljs-attr">"content"</span>: <span class="hljs-string">"Lorem ipsum dolor sit amet, consectetur adipiscing elit."</span>
    }
  }
}
</code></pre>
<p>result:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"evaluationResult"</span>: <span class="hljs-string">"{\"operation\":\"PutItem\",\"key\":{\"id\":{\"S\":\"927934a0-4fb4-4ee2-977a-4f4d458062fb\"}},\"attributeValues\":{\"createdAt\":{\"S\":\"2023-03-02T12:28:15.087Z\"},\"title\":{\"S\":\"Hello, world\"},\"content\":{\"S\":\"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\"},\"updatedAt\":{\"S\":\"2023-03-02T12:28:15.087Z\"}}}"</span>,
  <span class="hljs-attr">"logs"</span>: []
}
</code></pre>
<p>This looks a bit messy, but if you look closely, you will see that a DynamoDB <code>PutItem</code> request was generated. It contains our mocked data, which was enriched with the <code>createdAt</code> and <code>udpatedAt</code> fields from our helper function, as expected 🎉.</p>
<p>Before we call it a day, let me show you another way to test this.</p>
<p><a target="_blank" href="https://graphbolt.dev/?utm_campaign=js-resolvers-bundle&amp;utm_medium=Referral&amp;utm_source=blog.graphbolt.dev">GraphBolt</a> is a tool for developers to build, test and debug AppSync APIs. You can also use it to easily test JS resolvers in a user-friendly way thanks to the <strong>Mapping Template Designer</strong> tool it provides.</p>
<p>Copy the same bundled resolver code and paste it into the tool. Select <code>JavaScript</code> as the runtime. Finally, enter the mock data in the top section (under <code>args</code>).</p>
<p>Hit the <code>Eval</code> button, and when prompted, chose <code>request</code> as the function to be evaluated.</p>
<p>And voilà!</p>
<p>On the right hand, you should see the result of the code evaluation, with the expected <code>PutItem</code> request.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677760601365/0f22f746-6a2d-49f1-b020-ec7362bf13b3.png" alt class="image--center mx-auto" /></p>
<p>Now that we confirmed that the code is valid, you can use it in your AppSync API using your favorite IaC.</p>
<blockquote>
<p>💡 You can find the code for the example used in this tutorial, and more, on <a target="_blank" href="https://github.com/graphboltdev/appsync-js-resolver-bundle">GitHub</a>.</p>
</blockquote>
<h1 id="heading-conclusion">Conclusion</h1>
<p>The introduction of AppSync JavaScript resolvers was a game-changer. They let developers write code quicker in a language they are more familiar with. They also open the possibility to write reusable code more easily. In this article, I showed you how to write simple helper functions that you can use in more than one resolver while still following AppSync's rules. Finally, I showed you two ways you can both validate the bundled code and test its logic.</p>
]]></content:encoded></item><item><title><![CDATA[Building Modern Serverless  APIs with  CDK, Python, and GraphQL (Part 2)]]></title><description><![CDATA[In the first part of this article series, we gave a brief overview of the concept of event-driven architectures, coupling, and defined all AWS services needed to build the API.
Let's proceed.
Prerequisite
Before proceeding, please confirm you have al...]]></description><link>https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-2</link><guid isPermaLink="true">https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-2</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[Python]]></category><category><![CDATA[aws-cdk]]></category><dc:creator><![CDATA[Rosius Ndimofor]]></dc:creator><pubDate>Wed, 01 Mar 2023 08:32:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676823578439/16bab409-e557-48e9-b4ea-29d6d91e095a.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-1">first part</a> of this article series, we gave a brief overview of the concept of event-driven architectures, coupling, and defined all AWS services needed to build the API.</p>
<p>Let's proceed.</p>
<h2 id="heading-prerequisite">Prerequisite</h2>
<p>Before proceeding, please confirm you have all these dependencies installed on your computer</p>
<ul>
<li><p><a target="_blank" href="https://aws.amazon.com/cli/">AWS CLI</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html">AWS CDK</a></p>
</li>
</ul>
<h3 id="heading-creating-a-new-cdk-python-project">Creating a new CDK python project</h3>
<p>From the command line interface (Terminal), create and change the directory into the newly created folder using the command</p>
<p><code>mkdir eventDrivenCdk &amp;&amp; cd $_</code></p>
<p>I named my project <code>eventDrivenCdk</code>, feel free to give yours a different name.</p>
<p>Within the newly created project, initialize a python CDK project using the command</p>
<p><code>cdk init --language=python</code></p>
<p>This project is set up like a standard Python project. The initialization process also creates a <code>virtualenv</code> within this project, stored under the <code>.venv</code> directory.</p>
<p>To create the <code>virtualenv</code> it assumes that there is a <code>python3</code> (or <code>python</code> for Windows) executable in your path with access to the <code>venv</code>package.</p>
<p>If for any reason the automatic creation of the virtualenv fails, you can create the virtualenv manually.</p>
<p>To manually create a virtualenv on MacOS and Linux:</p>
<pre><code class="lang-plaintext">$ python3 -m venv .venv
</code></pre>
<p>After the init process completes and the virtualenv is created, you can use the following step to activate your virtualenv.</p>
<pre><code class="lang-plaintext">$ source .venv/bin/activate
</code></pre>
<p>If you are a Windows platform, you would activate the virtualenv like this:</p>
<pre><code class="lang-plaintext">% .venv\Scripts\activate.bat
</code></pre>
<p>Once the virtualenv is activated, you can install the required dependencies.</p>
<p>Add <code>boto3</code> to the <code>requirements.txt</code> before running the command.</p>
<pre><code class="lang-plaintext">$ pip install -r requirements.txt
</code></pre>
<p>Boto3 is the aws sdk for python.</p>
<h3 id="heading-graphql-schema">Graphql Schema</h3>
<p>In the root directory, create a file called <code>schema.graphql</code> and type in the following code. This file is a description of our Graphql API. It contains all the types, queries, mutations, and subscriptions for our Graphql API.</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Schema {
    <span class="hljs-symbol">query:</span> Query
    <span class="hljs-symbol">mutation:</span> Mutation
}

<span class="hljs-keyword">type</span> Order {
    <span class="hljs-symbol">name:</span> String!
    <span class="hljs-symbol">quantity:</span> Int!
    <span class="hljs-symbol">restaurantId:</span> String!
}

<span class="hljs-keyword">input</span> OrderInput {
    <span class="hljs-symbol">name:</span> String!
    <span class="hljs-symbol">quantity:</span> Int!
    <span class="hljs-symbol">restaurantId:</span> String!
}

<span class="hljs-keyword">input</span> UpdateOrderInput {
    <span class="hljs-symbol">id:</span> String!
    <span class="hljs-symbol">name:</span> String!
    <span class="hljs-symbol">quantity:</span> Int!
    <span class="hljs-symbol">restaurantId:</span> String!
}

<span class="hljs-keyword">type</span> Query {
    <span class="hljs-symbol">orders:</span> [Order ]!
    order(<span class="hljs-symbol">id:</span> String!): Order!
}

<span class="hljs-keyword">type</span> Mutation {
    postOrder(<span class="hljs-symbol">input:</span> OrderInput!): Order!
    updateOrder(<span class="hljs-symbol">input:</span> UpdateOrderInput!): Order!
    deleteOrder(<span class="hljs-symbol">id:</span> String!): String
}
</code></pre>
<p>From the schema, our API has 3 mutations and 2 queries. Before delving into the implementation details of these endpoints, we need to first define all the resources the app needs in order to run effectively.</p>
<h3 id="heading-defining-the-graphql-api-in-stack">Defining the GraphQL API in Stack</h3>
<p>The first step is to import the <code>appsync</code> class from the <code>aws-cdk-lib</code>.</p>
<p><code>import aws_cdk.aws_appsync as appsync</code></p>
<p>Then, use the <code>CfnGraphQLApi</code> method within the appsync class to create the API. This method takes a myriad of parameters, but for our use case, all we need is an API name, the authentication type, x-ray, and cloudwatch for tracing and logging.</p>
<pre><code class="lang-python">        api = appsync.CfnGraphQLApi(self, <span class="hljs-string">"Api"</span>,
                                    name=<span class="hljs-string">"event_driven_cdk"</span>,
                                    authentication_type=<span class="hljs-string">"API_KEY"</span>,
                                    xray_enabled=<span class="hljs-literal">True</span>,
                                    log_config=log_config
                                    )
   log_config = appsync.CfnGraphQLApi.LogConfigProperty(
            cloud_watch_logs_role_arn=appsync_cloud_watch_role.role_arn,
            exclude_verbose_content=<span class="hljs-literal">False</span>,
            field_log_level=<span class="hljs-string">"ALL"</span>)
</code></pre>
<p>This line <code>cloud_watch_logs_role_arn=appsync_cloud_watch_role.role_arn,</code> gives AppSync permissions to push logs to CloudWatch. Here's how we define the role and attach its policies.</p>
<pre><code class="lang-python">  cloud_watch_role_full_access = iam.ManagedPolicy.from_managed_policy_arn(self, <span class="hljs-string">"cloudWatchLogRole"</span>,
                                                                                 <span class="hljs-string">'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess'</span>)

 appsync_cloud_watch_role = iam.Role(self, <span class="hljs-string">"AppSyncCloudWatchRole"</span>,
                                            assumed_by=iam.ServicePrincipal(<span class="hljs-string">"appsync.amazonaws.com"</span>),
                                            managed_policies=[
                                                cloud_watch_role_full_access
                                            ])
</code></pre>
<p>After creating the API, the next logical step is to attach the schema. We use the <code>CfnGraphQLSchema</code> method from the AppSync class to achieve this. This method takes in a scope, an id, an api_id which should be a unique AWS Appsync GraphQL API identifier, and a definition (the schema file itself).</p>
<pre><code class="lang-python">
dirname = path.dirname(__file__)

<span class="hljs-keyword">with</span> open(os.path.join(dirname, <span class="hljs-string">"../schema.graphql"</span>), <span class="hljs-string">'r'</span>) <span class="hljs-keyword">as</span> file:
    data_schema = file.read().replace(<span class="hljs-string">'\n'</span>, <span class="hljs-string">''</span>)
schema = appsync.CfnGraphQLSchema(scope=self, id=<span class="hljs-string">"schema"</span>, api_id=api.attr_api_id, definition=data_schema)
</code></pre>
<h3 id="heading-defining-the-queue">Defining the Queue</h3>
<p>Let's define and attach the SQS queue to AppSync. Firstly, we import the SQS class from cdk.</p>
<p><code>import aws_cdk.aws_sqs as sqs</code></p>
<p>Then, we'll create 2 queues and use one as the Dead letter queue (DLQ). A Dead letter Queue is a message queue that'll store all the messages that couldn't be processed successfully. The developer can always go back to the DLQ and redrive the unsuccessful messages.</p>
<pre><code class="lang-python">        <span class="hljs-comment"># SQS</span>
        queue = sqs.CfnQueue(
            self, <span class="hljs-string">"CdkAccelerateQueue"</span>,
            visibility_timeout=<span class="hljs-number">300</span>,
            queue_name=<span class="hljs-string">"sqs-queue"</span>
        )

        deadLetterQueue = sqs.Queue(
            self, <span class="hljs-string">"CdkAccelerateDLQueue"</span>,
            visibility_timeout=Duration.minutes(<span class="hljs-number">10</span>),
            queue_name=<span class="hljs-string">"dead-letter-queue"</span>
        )

        sqs.DeadLetterQueue(max_receive_count=<span class="hljs-number">4</span>, queue=deadLetterQueue)
</code></pre>
<p>The <code>visibility_timeout</code> is the time taken for a consumer to process and delete a message once dequeued. While this timeout is valid, the message is made unavailable to other consumers. If the timeout expires when the message hasn't been successfully processed and delivered, the message is sent back into the queue and made available for other consumers to pick up.</p>
<p>If you don't specify a value for the <code>visibility_timeout</code>, AWS CloudFormation uses the default value of 30 seconds. <code>Default: Duration.seconds(30)</code></p>
<p>The <code>max_receive_count</code> is the number of times a message can be unsuccessfully dequeued before being moved to the dead-letter queue. We set the value to 4, meaning after 4 unsuccessful dequeue attempts, that message would be sent to the DLQ.</p>
<p>Now, let's attach the SQS queue to Appsync.</p>
<p><code>api.add_dependency(queue)</code></p>
<p>That's all. Remember the name of the GraphQL API we created above was <code>api</code>.</p>
<h3 id="heading-defining-dynamodb-resources">Defining DynamoDB Resources</h3>
<p>Import the dynamodb class for cdk.</p>
<p><code>import aws_cdk.aws_dynamodb as dynamodb</code></p>
<p>Create a table called <code>ORDER</code> with a composite key.</p>
<p><code>user_id</code> for the primary key and <code>id</code> as the sort key.</p>
<pre><code class="lang-python">        <span class="hljs-comment"># DynamoDB</span>
        dynamodb.CfnTable(self, <span class="hljs-string">"Table"</span>,
                          key_schema=[dynamodb.CfnTable.KeySchemaProperty(
                              attribute_name=<span class="hljs-string">"user_id"</span>,
                              key_type=<span class="hljs-string">"HASH"</span>
                          ),
                              dynamodb.CfnTable.KeySchemaProperty(
                                  attribute_name=<span class="hljs-string">"id"</span>,
                                  key_type=<span class="hljs-string">"RANGE"</span>
                              )],
                          billing_mode=<span class="hljs-string">"PAY_PER_REQUEST"</span>,
                          table_name=<span class="hljs-string">"ORDER"</span>,
                          attribute_definitions=[dynamodb.CfnTable.AttributeDefinitionProperty(
                              attribute_name=<span class="hljs-string">"user_id"</span>,
                              attribute_type=<span class="hljs-string">"S"</span>
                          ),
                              dynamodb.CfnTable.AttributeDefinitionProperty(
                                  attribute_name=<span class="hljs-string">"id"</span>,
                                  attribute_type=<span class="hljs-string">"S"</span>
                              )]
                          )
</code></pre>
<h3 id="heading-defining-sns-resources">Defining SNS Resources</h3>
<p>Create an SNS topic with the topic name <code>sns-topic</code>. As usual, we'll import the SNS class from AWS CDK</p>
<p><code>from aws_cdk import aws_sns as sns</code></p>
<p>Then use <code>CfnTopic</code> and <code>CfnTopicPolicy</code> methods from the SNS class to create and grant policies to the SNS topic.</p>
<pre><code class="lang-python">        cfn_topic = sns.CfnTopic(self, <span class="hljs-string">"MyCfnTopic"</span>,
                                 display_name=<span class="hljs-string">"sns-topic"</span>,
                                 fifo_topic=<span class="hljs-literal">False</span>,
                                 topic_name=<span class="hljs-string">"sns-topic"</span>
                                 )

        sns_publish_policy = sns.CfnTopicPolicy(self, <span class="hljs-string">"MyCfnTopicPolicy"</span>,
                                                policy_document=iam.PolicyDocument(
                                                    statements=[iam.PolicyStatement(
                                                        actions=[<span class="hljs-string">"sns:Publish"</span>, <span class="hljs-string">"sns:Subscribe"</span>
                                                                 ],
                                                        principals=[iam.AnyPrincipal()],
                                                        resources=[<span class="hljs-string">"*"</span>]
                                                    )]
                                                ),
                                                topics=[cfn_topic.attr_topic_arn]
                                                )
</code></pre>
<p>Bear in mind that, we've assumed the client is already signed in to our application at this point. So we have their email address. We'll use that email address to subscribe to the SNS topic to receive email updates on <code>success</code> or <code>failed</code> order payments.</p>
<p>For the purpose of this tutorial, we'll pass in the email address as a parameter from the CLI, when deploying the application later.</p>
<p>For now, we need to add <code>email</code> as a subscriber using the <code>CfnSubscription</code> method from the SNS class.</p>
<pre><code class="lang-python">        email_address = CfnParameter(self, <span class="hljs-string">"subscriptionEmail"</span>)
        sns.CfnSubscription(self, <span class="hljs-string">"EmailSubscription"</span>,
                            topic_arn=cfn_topic.attr_topic_arn,
                            protocol=<span class="hljs-string">"email"</span>,
                            endpoint=email_address.value_as_string

                            )
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this episode,</p>
<ul>
<li><p>We Created A GraphQL API and attached a schema to it.</p>
</li>
<li><p>Created and attached a queue to the AppSync API.</p>
</li>
<li><p>Created a Dead Letter Queue to catch unprocessed messages.</p>
</li>
<li><p>Created an SNS topic, with an email as a subscriber.</p>
</li>
<li><p>Gave IAM roles and policies to all created resources.</p>
</li>
</ul>
<p>In the next episode, we'll continue creating more resources for the API. Please stay tuned. Thanks for reading. If you enjoyed it, please leave a like or a comment. I'll love to know what you think. Take Care.</p>
]]></content:encoded></item><item><title><![CDATA[Building Modern Serverless  APIs with  CDK, Python and GraphQL (Part 1)]]></title><description><![CDATA[The World is Asynchronous
-- Dr Werner Vogels ReInvent 2022

The concept of Event Driven Architecture (EDA) is not new. As a matter of fact, it's been around for ages.
But lately, it's been the talk of every tech clique. There's no way you can scroll...]]></description><link>https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-1</link><guid isPermaLink="true">https://blog.graphbolt.dev/building-modern-serverless-apis-with-cdk-python-and-graphql-part-1</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[AWS]]></category><category><![CDATA[event-driven-architecture]]></category><dc:creator><![CDATA[Rosius Ndimofor]]></dc:creator><pubDate>Wed, 22 Feb 2023 08:00:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676823689959/2d709a81-dd93-4115-aad2-8c22cc254bc6.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>The World is Asynchronous</p>
<p>-- Dr Werner Vogels ReInvent 2022</p>
</blockquote>
<p>The concept of <code>Event Driven Architecture (EDA)</code> is not new. As a matter of fact, it's been around for ages.</p>
<p>But lately, it's been the talk of every tech clique. There's no way you can scroll through tech Twitter without coming across a tweet concerning EDA.</p>
<p>But what are EDA's and why is every organization adopting this architectural pattern?</p>
<h3 id="heading-event-driven-architectures">Event Driven Architectures?</h3>
<p>Minimizing the degree of interdependence between different components of a system is the main consideration when building event-driven architectures. The term <code>loosely coupled</code> or <code>Decoupling</code> is generally used.</p>
<p>Lots of integration patterns have been created throughout the years, to tackle coupling within an application.</p>
<p>So how do you determine the level of coupling within your application's components and what strategies do you use in order to guarantee a loosely coupled system?</p>
<p>IT strategist and cloud/enterprise Architect, Gregor Hohpe Says</p>
<blockquote>
<p>The appropriate level of coupling depends on the level of control you have over the endpoints</p>
</blockquote>
<p>Generally, if your application depends on third-party APIs which you have less control over, you don't want to build a strong attachment with those APIs. Because, if they go down, your application goes down.</p>
<p>You want a scenario where you'll be able to easily pull back your resources in case of a system hiccup. This goes same for all the other components of your system, whether internal or external.</p>
<p>As we progress through the application, we'll see how to use AWS Services to ensure your system is loosely coupled</p>
<p>EDA's use events to coordinate communication between loosely coupled services.</p>
<p>But what are events?</p>
<h2 id="heading-events">Events</h2>
<p>An event is a change in state, or an update, like an order added to the cart in a restaurant application. Events can either carry the state (the item purchased, its price, and quantity) or events can be identifiers (a notification that a payment was successful or not).</p>
<p>EDAs are made up of 3 parts.</p>
<h3 id="heading-event-producers">Event Producers</h3>
<p>This can be a mobile app or an e-commerce site</p>
<h3 id="heading-event-routers">Event Routers</h3>
<p>This can be an event store</p>
<h3 id="heading-event-consumers">Event Consumers</h3>
<p>This can be a database, saas application, microservice, etc</p>
<p>So an event producer publishes an event to the event router, the event router filters and pushes the event to event consumers. The event router abstracts the producer from the consumer by allowing them to communicate asynchronously.</p>
<h2 id="heading-why-you-should-adopt-eda">Why you should adopt EDA</h2>
<ul>
<li><p>With services that scale on demand and fail independently, your application is bound to be reliable and resilient.</p>
</li>
<li><p>Event routers coordinate events automatically between producers and consumers, so you no longer need to write code to handle that.</p>
</li>
<li><p>Event-driven architectures are push-based, so everything happens on-demand as the event presents itself in the router. This way, you’re not paying for continuous polling to check for an event.</p>
</li>
<li><p>EDAs are responsive to the needs of customers as they expect systems to respond to events as they happen.</p>
</li>
<li><p>EDAs are cost effective, allowing customers to pay only for the services they need when they need them.</p>
</li>
</ul>
<p>Now that we have a brief understanding of what EDAs are, let's go ahead and dive into the main topic for the blog post.</p>
<h2 id="heading-article-scope">Article Scope</h2>
<p>We'll use the concept of EDA to design and build a modern serverless Graphql API on AWS. We will be doing so, by mixing and matching different AWS services in a technical manner.</p>
<h2 id="heading-out-of-scope">Out Of Scope</h2>
<p>We'll not be looking at introductions to any of the AWS Services used in this tutorial such as</p>
<ul>
<li><p>AWS Step functions</p>
</li>
<li><p>AWS SNS</p>
</li>
<li><p>AWS SQS</p>
</li>
</ul>
<p>But I'll provide supporting documents to upskill if need be.</p>
<p>This tutorial is aimed at the following audiences:</p>
<ul>
<li>Software engineers looking for a quick hands-on intro to Event-Driven Architectures</li>
</ul>
<h2 id="heading-use-case">Use Case</h2>
<p>Let's say you want to order pizza from a restaurant, through the restaurant's mobile or web application.</p>
<p>In an ideal scenario, the application would let you make (add to cart) your choice of pizza, and the quantity you want, with all necessary add-ons like extra cheese, and chili, and only let you sign in to or create an account when you wish to place the order.</p>
<p>In this tutorial, we'll assume you've already created an account, and you are about to place an order. Once your order has been placed, we'll run a fake payment check to determine if you've made payment or not, and would email you based on the status of the order, be it <code>success</code> or <code>failure</code>.</p>
<p>We'll be using a couple of AWS Services to illustrate how all these can be accomplished, in a scalable, decoupled, event-driven manner.</p>
<p>Here are the AWS services we'll be using in this application.</p>
<h3 id="heading-aws-services">AWS Services</h3>
<h4 id="heading-aws-appsynchttpsawsamazoncomappsync"><a target="_blank" href="https://aws.amazon.com/appsync/">AWS AppSync</a></h4>
<p>AWS AppSync is a fully managed service allowing developers to deploy scalable and engaging real-time GraphQL backends on AWS.</p>
<p>It leverages WebSockets connections under the hood to provide real-time capabilities, by publishing data updates to connected subscribers.</p>
<h4 id="heading-amazon-sqshttpsawsamazoncomsqs"><a target="_blank" href="https://aws.amazon.com/sqs/">Amazon SQS</a></h4>
<p>A fully managed message queueing service to decouple producers and consumers. SQS is a fundamental building block for building decoupled architectures</p>
<h4 id="heading-aws-lambdahttpsawsamazoncomlambda"><a target="_blank" href="https://aws.amazon.com/lambda/">AWS Lambda</a></h4>
<p>AWS Lambda is a serverless, event-driven compute service that lets you run code for virtually any type of application or backend service without provisioning or managing servers. You can trigger Lambda from over 200 AWS services and software-as-a-service (SaaS) applications and only pay for what you use.</p>
<h4 id="heading-aws-stepfunctionshttpsawsamazoncomstep-functions"><a target="_blank" href="https://aws.amazon.com/step-functions/">AWS StepFunctions</a></h4>
<p>AWS Step Functions is a visual workflow service that helps developers use AWS services to build distributed applications, automate processes, orchestrate microservices, and create data and machine learning (ML) pipelines.</p>
<h4 id="heading-aws-snshttpsawsamazoncomsns"><a target="_blank" href="https://aws.amazon.com/sns/">AWS SNS</a></h4>
<p>Amazon Simple Notification Service (SNS) sends notifications in two ways, Application-to-Application (A2A) and Application-to-Person (A2P).</p>
<p>A2A provides high-throughput, push-based, many-to-many messaging between distributed systems, microservices, and event-driven serverless applications. These applications include Amazon Simple Queue Service (SQS), Amazon Kinesis Data Firehose, AWS Lambda, and other HTTPS endpoints.</p>
<p>A2P functionality lets you send messages to your customers with SMS texts, push notifications, and email.</p>
<p>For this application, we'll be using A2P.</p>
<h4 id="heading-amazon-dynamodbhttpsawsamazoncomdynamodb"><a target="_blank" href="https://aws.amazon.com/dynamodb/">Amazon DynamoDB</a></h4>
<p>Amazon DynamoDB is a fully managed, serverless, key-value NoSQL database designed to run high-performance applications at any scale. DynamoDB offers built-in security, continuous backups, automated multi-Region replication, in-memory caching, and data import and export tools.</p>
<h4 id="heading-solutions-architecture">Solutions Architecture</h4>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/eda_cdk_svg2.svg" alt="alt text" /></p>
<p>From the architecture above, the Amplify icon signifies a front-end application. It can be a mobile or web app. So a client places an order for 5 cartons of pizza from Domino's pizza.</p>
<p>Here's the request payload</p>
<pre><code class="lang-json"><span class="hljs-string">"name"</span>: <span class="hljs-string">"Pizza"</span>,
<span class="hljs-string">"quantity"</span>: <span class="hljs-number">6</span>,
<span class="hljs-string">"restaurantId"</span>: <span class="hljs-string">"Dominos</span>
</code></pre>
<p>AppSync receives the request and sends a message, containing the payload to an attached SQS queue. We use SQS to decouple the event producers (AppSync) from the consumers (in this case a Lambda), so that they can communicate asynchronously.</p>
<p>So as order requests keep flooding in, let's say on a world pizza day when demand is really high, all requests are being sent to SQS. Lambda is a common choice as a consumer for SQS as it supports native integration. So you get to write and maintain less code between both of them.</p>
<p>When a Lambda function subscribes to an SQS queue, Lambda polls the queue as it waits for messages to arrive. Lambda consumes messages in batches, starting at five concurrent batches with five functions at a time. If there are more messages in the queue, Lambda adds up to 60 functions per minute, and up to 1,000 functions, to consume those messages.</p>
<p>This means that Lambda can scale up to 1,000 concurrent Lambda functions processing messages from the SQS queue.</p>
<p><img src="https://raw.githubusercontent.com/trey-rosius/event_driven_cdk/master/images/lambda_sqs_backgroud.svg" alt="alt text" /></p>
<p>Failed messages are sent back into the queue, to be retried by Lambda. A DLQ (Dead letter queue) is put in place to prevent failed messages from getting added to the queue multiple times.</p>
<p>Once in DLQ, these messages can be reassessed and resent to the Lambda for processing by humans, through a redrive policy.</p>
<p>Once Lambda successfully processes a message, it extracts the order payload and invokes a step functions workflow with the payload as the input request.</p>
<p>We use step functions to orchestrate the payment process. No real APIs are being called. All we do is mimic a real-life scenario.</p>
<p>Inside the Step Functions, we randomly determine if an order was paid or not, save the order into DynamoDB, and then send <code>success</code> or <code>failure</code> emails using SNS to the client who made the order.</p>
<p>We also create endpoints to <code>get_order</code>, <code>update_order</code>, and <code>delete_order</code>.</p>
<p>Enough talk. Let's see some code.</p>
<p>🚨Note: We would be looking at code snippets and not the complete source code for the application.</p>
<p>For the complete code, please visit the GitHub page at <a target="_blank" href="https://github.com/trey-rosius/event_driven_cdk">Event Driven CDK</a></p>
<p>Let's end here and continue in part 2.</p>
<p>Hope you enjoyed reading this piece as I enjoyed writing it.</p>
<p>Do you see any errors?</p>
<p>Have any suggestions?</p>
<p>Loved the article?</p>
<p>Please like or leave a comment.</p>
<p>See you in part 2✌🏾</p>
]]></content:encoded></item><item><title><![CDATA[Everything You Should Know About the AppSync JavaScript Pipeline Resolvers]]></title><description><![CDATA[In November 2022, AWS announced JavaScript support for AppSync Resolvers. It was a long-awaited feature that everyone was excited about. 
In this article, I will try to shed some light on this new feature, what JS resolvers are, how they can improve ...]]></description><link>https://blog.graphbolt.dev/everything-you-should-know-about-the-appsync-javascript-pipeline-resolvers</link><guid isPermaLink="true">https://blog.graphbolt.dev/everything-you-should-know-about-the-appsync-javascript-pipeline-resolvers</guid><category><![CDATA[AWS]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Benoît Bouré]]></dc:creator><pubDate>Sun, 27 Nov 2022 10:26:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1669222645134/ZD6pE2a4l.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In November 2022, AWS <a target="_blank" href="https://aws.amazon.com/blogs/aws/aws-appsync-graphql-apis-supports-javascript-resolvers/">announced JavaScript support for AppSync Resolvers</a>. It was a long-awaited feature that everyone was excited about. </p>
<p>In this article, I will try to shed some light on this new feature, what JS resolvers are, how they can improve developer experience, what possibilities they unlock; but also their limitations.</p>
<h1 id="heading-what-js-resolvers-are-not">What JS Resolvers Are Not</h1>
<p>Before we start, it is important to make some clarification about what JS resolvers are not. There might be some misconceptions that will arise, mostly because they allow writing resolvers in a way that many developers already write their Lambda functions with (i.e. JavaScript). We should clear that out of the way from the beginning: <strong>JS resolvers are not a replacement for Lambda function resolvers</strong>, they will never be. You won't be able to do whatever you want; they come with a set of limitations that we will explore later. They are, however, a good alternative to Lambda  in some cases and should simplify the choice between both options.</p>
<h1 id="heading-what-are-js-resolvers-then">What Are JS Resolvers, Then?</h1>
<p>They are a new way for AppSync to communicate with your data sources. This is what we have always known as “mapping templates”; except that instead of writing them in the VTL language, you can now use (a subset of) JavaScript, which brings tons of benefits (see below). <strong>You still need to attach your JS resolvers to a data source</strong>, and the only thing they should do is generate a request for that data source and format the response it returns. </p>
<p>From the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#writing-resolvers">doc</a>:</p>
<blockquote>
<p>The request handler takes a context object as an argument and returns a request payload in the form of a JSON object used to call your data source. The response handler receives a payload back from the data source with the result of the executed request</p>
</blockquote>
<h1 id="heading-what-benefits-do-they-bring">What benefits do they bring?</h1>
<p>Now that we made clear what JS resolvers are, let's talk about their benefits; because there are! (other than "I don't like VTL 🤓")</p>
<p><strong>Better Developer Exprience</strong></p>
<p>Being able to write AppSync handlers in JavaScript obviously makes it easier for everyone. JS is a language that most developers are familiar with, and it has so many benefits over VTL such as better IDE support or <a target="_blank" href="https://www.npmjs.com/package/@aws-appsync/eslint-plugin">code linting</a>. If you want type support, you can also write them in TypeScript as long as you transpile them to JavaScript later.</p>
<p><strong>Room for extension</strong></p>
<p>JavaScript makes it possible to write reusable code by creating custom functions, utilities, or libraries. This was not (easily) possible with VTL. This should make it easier to write better code faster.</p>
<p><strong>No cold start and no extra cost</strong></p>
<p>JS resolvers, unlike Lambda functions, will not suffer cold start, and there is no penalty for choosing JS over VTL in terms of performance. They are also available at no extra cost. Being able to write resolver handlers as JS will potentially allow many to get rid of unnecessary Lamba functions; reducing latency and cost at the same time.</p>
<h1 id="heading-limitations">Limitations</h1>
<p>If JS resolvers have some benefits, you should be aware of their limitations. These limitations might evolve over time, but some of them will likely never go away. Keep in mind that if JS resolvers don't allow you to do some things, it might as well be to <strong>protect you from using bad practices</strong>.</p>
<p><strong>Only Pipeline resolvers are supported</strong></p>
<p>For the time being, only Pipeline resolvers are supported. Unit resolvers still require the usage of VTL mapping templates. </p>
<blockquote>
<p>Hint: 💡 If you still want to use JS with a unit resolver, you can always write a pipeline resolver that has only one function.</p>
</blockquote>
<p><strong>Unsupported JS features</strong></p>
<p>It is primordial to remember that JS resolvers use a <strong>subset</strong> of  ECMAScript 6. It means that all features are not available. 
For example, you can't use:  <code>for</code> loops (<code>for-in</code> and <code>for-of</code> <a target="_blank" href="https://twitter.com/mohit/status/1593937516557897728">are supported</a>), <code>try</code>, <code>catch</code>, <code>finally</code>,  <code>continue</code>, <code>do-while</code> or <code>while</code> loops.</p>
<p><code>throw</code> is also not supported. If you want to "throw" an error, you should use the <code>util.error()</code> util, which is basically the equivalent to the <code>$ctx.util.error()</code> that you already know. </p>
<p>Arrow functions are not supported either.</p>
<p><strong>No Async/Await</strong></p>
<p><code>async/await</code> are not supported, for obvious reasons. JS resolvers should be completely synchronous and return as fast as possible.</p>
<p><strong>No network access</strong></p>
<p>You won't be able to do any network request in a JS resolver (e.g. you can't use fetch/axios). </p>
<blockquote>
<p>💡 Hint: If you need to do that you probably want to use an <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-http-resolvers.html">HTTP resolver</a> instead.</p>
</blockquote>
<p><strong>Size limitation and <code>import</code></strong></p>
<p>If you follow the above rules, you are free to do everything you want. While you <strong>can't</strong> use the <code>import</code> statement (except with <code>@aws-appsync/utils</code>), you should be able to include custom libraries and code <strong>if you bundle them in a single file</strong> before you upload them to AppSync. (e.g. using <code>esbuild</code>).</p>
<p>There is a big catch though: all the code in the bundle MUST be a valid ES6 module, follow all the above AppSync JS rules, and the final package must not exceed 32KB.</p>
<p>Unfortunately, this means that you will likely never be able to include almost any npm package out there at the moment. But, it opens the door to creating dedicated AppSync-compatible libraries that strictly follow the requirements. A quick POC shows that this is possible.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/Benoit_Boure/status/1594686086000828423">https://twitter.com/Benoit_Boure/status/1594686086000828423</a></div>
<p>Please refer to the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-util-reference-js.html">documentation</a> for a full list of supported and non-supported features.</p>
<h1 id="heading-what-else-has-changed">What Else Has Changed?</h1>
<p><strong>The AppSync utils</strong></p>
<p>In VTL, we were used to invoking functions like <code>$util.dynamodb.toDynamoDBJson()</code>. Those utilities still make sense and are useful in JS. They now live in a new npm package: <a target="_blank" href="https://www.npmjs.com/package/@aws-appsync/utils">@aws-appsync/utils</a>.</p>
<p>This new package also contains an API for interacting with AppSync: <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/extensions-js.html"><code>extensions</code></a>.</p>
<p><strong>CloudWatch logs</strong></p>
<p>Those who, like me, were used to diving into the CloudWatch AppSync logs will notice new log types:  <code>BeforeRequestFunctionEvaluation</code>, <code>RequestFunctionEvaluation</code>, <code>ResponseFunctionEvaluation</code>, <code>AfterResponseFunctionEvaluation</code>.</p>
<p>These log records are equivalent to the ones that you used to see when debugging VTL mapping templates (namely <code>BeforeMapping</code>, <code>AfterMapping</code>, <code>RequestMapping</code>, and <code>ResponseMapping</code>), but they are specific to JS resolvers. They look roughly the same as their VTL counterpart. </p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"logType"</span>: <span class="hljs-string">"RequestFunctionEvaluation"</span>,
  <span class="hljs-attr">"fieldName"</span>: <span class="hljs-string">"getPost"</span>,
  <span class="hljs-attr">"resolverArn"</span>: <span class="hljs-string">"arn:aws:appsync:us-east-1:12345678912:apis/abcdefghijklmnop/types/Query/resolvers/getPost"</span>,
  <span class="hljs-attr">"functionName"</span>: <span class="hljs-string">"getPost"</span>,
  <span class="hljs-attr">"fieldInError"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"evaluationResult"</span>: {
    <span class="hljs-attr">"operation"</span>: <span class="hljs-string">"GetItem"</span>,
    <span class="hljs-attr">"key"</span>: {
      <span class="hljs-attr">"id"</span>: {
        <span class="hljs-attr">"S"</span>: <span class="hljs-string">"b03a2a76-1f2c-4150-9b3c-aab88a04f49e"</span>
      }
    }
  },
  <span class="hljs-attr">"parentType"</span>: <span class="hljs-string">"Query"</span>,
  <span class="hljs-attr">"path"</span>: [
    <span class="hljs-string">"getPost"</span>
  ],
  <span class="hljs-attr">"requestId"</span>: <span class="hljs-string">"83e25c50-8d97-4939-8475-440c0ed8b36d"</span>,
  <span class="hljs-attr">"context"</span>: {
    <span class="hljs-attr">"arguments"</span>: {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"b03a2a76-1f2c-4150-9b3c-aab88a04f49e"</span>
    },
    <span class="hljs-attr">"prev"</span>: {
      <span class="hljs-attr">"result"</span>: {}
    },
    <span class="hljs-attr">"stash"</span>: {},
    <span class="hljs-attr">"outErrors"</span>: []
  },
  <span class="hljs-attr">"errors"</span>: [],
  <span class="hljs-attr">"graphQLAPIId"</span>: <span class="hljs-string">"ykwu6mw2mrc3ho4jczeg6p42pa"</span>,
  <span class="hljs-attr">"functionArn"</span>: <span class="hljs-string">"arn:aws:appsync:us-east-1:12345678912:apis/abcdefghijklmnop/functions/uvwyyz"</span>
}
</code></pre>
<p>You can also write custom logs by using the <code>console.log()</code> function. You will see them appear in CloudWatch.</p>
<pre><code><span class="hljs-number">83e25</span>c50<span class="hljs-number">-8</span>d97<span class="hljs-number">-4939</span><span class="hljs-number">-8475</span><span class="hljs-number">-440</span>c0ed8b36d INFO - code.js:<span class="hljs-number">5</span>:<span class="hljs-number">3</span>: <span class="hljs-string">"Hello from JS Resolver!"</span>
</code></pre><h1 id="heading-conclusion">Conclusion</h1>
<p>JS resolvers will greatly improve the developer experience. They should help developers adopt direct integration with data sources (e.g. DynamoDB) more often, while Lambda functions are still useful for more advanced and complex use cases.</p>
<p>There is still a lot to explore, though. This is only the beginning. I am sure the community will come up with great ways to use them and extend them 🚀</p>
<p><em>Credits: Cover image generated by <a target="_blank" href="https://openai.com/dall-e-2/">dall·e</a></em></p>
]]></content:encoded></item></channel></rss>