<?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[Michael Liendo | Focus Otter]]></title><description><![CDATA[Sr. Developer Advocate @ AWS AppSync, formerly AWS Amplify]]></description><link>https://blog.focusotter.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1629772791716/RExw9ZVQt.png</url><title>Michael Liendo | Focus Otter</title><link>https://blog.focusotter.com</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 13:01:19 GMT</lastBuildDate><atom:link href="https://blog.focusotter.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Complete Guide to Integrating Clerk with an AWS Backend]]></title><description><![CDATA[Don't get me wrong: I love how simple Amazon Cognito makes it to get email authentication working. It even has direct support for Google and Facebook. But what about authenticating with Notion, GitHub, Slack, etc? That's when I reach for Clerk. Clerk...]]></description><link>https://blog.focusotter.com/the-complete-guide-to-integrating-clerk-with-an-aws-backend</link><guid isPermaLink="true">https://blog.focusotter.com/the-complete-guide-to-integrating-clerk-with-an-aws-backend</guid><category><![CDATA[AWS]]></category><category><![CDATA[AWS Amplify]]></category><category><![CDATA[focusotter]]></category><category><![CDATA[aws-cdk]]></category><category><![CDATA[Startups]]></category><category><![CDATA[full stack]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Developer]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[lambda]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Mon, 10 Jun 2024 22:25:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1718054901215/c4b72cba-3d2a-4c47-ad98-10095ce4f925.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Don't get me wrong: I love how simple Amazon Cognito makes it to get email authentication working. It even has direct support for Google and Facebook. But what about authenticating with Notion, GitHub, Slack, etc? That's when I reach for <a target="_blank" href="https://clerk.com/">Clerk</a>. Clerk is more than just a auth provider. It's an entire user management platform that makes it really simple to get authentication setup.</p>
<p>However, adding a third-party SaaS when the rest of your application backend is built on AWS can seem tricky. That's why in this post, I'm going to show <em>exactly</em> how to put the two together.</p>
<h2 id="heading-application-overview">Application Overview</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/fullstack-with-clerk-auth">https://github.com/focusOtter/fullstack-with-clerk-auth</a></div>
<p> </p>
<blockquote>
<p>🗒️ All the code relating to this post can be found in the repo above.</p>
</blockquote>
<p>Typically, when I add authorization with AWS, it's with Amazon Cognito. In Amplify Gen 2, this is done for you since when you scaffold your application with <code>npm create amplify</code>, both a <code>data</code> and <code>auth</code> resource are created. However, the underlying API (<a target="_blank" href="https://aws.amazon.com/appsync/">AWS AppSync</a>) has a trick up its sleeve: You can create a Lambda function to have complete control over how the authentication works. This Lambda function can then be set as the authentication mechanism on the API instead of a Cognito userpool.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> data = defineData({
    name: <span class="hljs-string">'fullstack-with-clerk'</span>,
    schema,
    authorizationModes: {
        defaultAuthorizationMode: <span class="hljs-string">'lambda'</span>, <span class="hljs-comment">// set the mode</span>
        lambdaAuthorizationMode: {
            <span class="hljs-function"><span class="hljs-keyword">function</span>: <span class="hljs-title">APIAuthorizer</span>, // <span class="hljs-title">apply</span> <span class="hljs-title">the</span> <span class="hljs-title">lambda</span> <span class="hljs-function"><span class="hljs-keyword">function</span>
        },
    },
})</span></span>
</code></pre>
<h2 id="heading-understanding-aws-appsync-lambda-authorizers">Understanding AWS AppSync Lambda Authorizers</h2>
<p>Lambda authorizers in AWS AppSync work differently than those found in other API services on AWS. In my opinion, they're much simpler.</p>
<p>In short, as long as you return an object with an <code>isAuthorized</code> boolean, you're all set. How you form the <code>true</code> or <code>false</code> value for that boolean is up to you.</p>
<p>Any authorization tokens passed to our API will appear on the <code>event.authorizationToken</code> parameter. In our application, we'll use the JWT of the user from Clerk, verify it, and then decode it to get the users <code>sub</code>. If all of that works, then we know they are authorized:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Handler } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-lambda'</span>
<span class="hljs-keyword">import</span> jwt, { JwtPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'jsonwebtoken'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> crypto <span class="hljs-keyword">from</span> <span class="hljs-string">'crypto'</span>
<span class="hljs-keyword">import</span> { env } <span class="hljs-keyword">from</span> <span class="hljs-string">'$amplify/env/clerk-api-authorizer'</span>

<span class="hljs-keyword">const</span> jwkToPem = <span class="hljs-function">(<span class="hljs-params">jwk: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> keyObject = crypto.createPublicKey({
        key: jwk,
        format: <span class="hljs-string">'jwk'</span>,
    })

    <span class="hljs-keyword">const</span> pem = keyObject.export({
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'spki'</span>,
        format: <span class="hljs-string">'pem'</span>,
    })

    <span class="hljs-keyword">return</span> pem
}
<span class="hljs-comment">//make sure lambda runtime supports `fetch` (v &gt;= 20)</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler: Handler = <span class="hljs-keyword">async</span> (event: {
    authorizationToken: <span class="hljs-built_in">string</span>
}) =&gt; {
    <span class="hljs-keyword">const</span> token = event.authorizationToken
    <span class="hljs-keyword">const</span> secret = env.CLERK_API_KEY
    <span class="hljs-keyword">const</span> validDomains = [<span class="hljs-string">'http://localhost:5173'</span>]

    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'https://api.clerk.com/v1/jwks'</span>, {
        headers: {
            Authorization: <span class="hljs-string">`Bearer <span class="hljs-subst">${secret}</span>`</span>,
        },
    })

    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json()
    <span class="hljs-keyword">const</span> pem = jwkToPem(data.keys[<span class="hljs-number">0</span>])

    <span class="hljs-keyword">let</span> decoded: JwtPayload | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>
    <span class="hljs-keyword">try</span> {
        decoded = jwt.verify(token, pem) <span class="hljs-keyword">as</span> JwtPayload
    } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'the error'</span>, e)
        <span class="hljs-keyword">return</span> <span class="hljs-string">'Invalid token'</span>
    }

    <span class="hljs-keyword">if</span> (!decoded || !validDomains.includes(decoded.azp))
        <span class="hljs-keyword">return</span> { isAuthorized: <span class="hljs-literal">false</span> }

    <span class="hljs-keyword">return</span> {
        isAuthorized: decoded.sub ? <span class="hljs-literal">true</span> : <span class="hljs-literal">false</span>,
        resolverContext: { owner: decoded.sub },
    }
}
</code></pre>
<p>A couple of notes on the code above:</p>
<ol>
<li><p>The <code>env</code> object from Amplify is Gen 2 specific. More details can be found <a target="_blank" href="https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/#local-environment">here</a>. If not using Gen 2 (AWS CDK), you'll have to get the Clerk secret from SSM or another mechanism.</p>
</li>
<li><p>The endpoint <code>'https://api.clerk.com/v1/jwks'</code> was figured out by following the Clerk docs for <a target="_blank" href="https://clerk.com/docs/backend-requests/handling/manual-jwt">manually verifying the JWT</a>. The rest is just getting the returned keys, generating a pem file, and verifying the token with the pem.</p>
</li>
<li><p>In the return statement, I'm passing <code>resolverContext</code>. Think of this as metadata that by API resolvers can then make use of. It will be available as <code>context.identity.resolverContext.owner</code>. If you just want authenticated users to access your API, then this is not needed, but this is important is you want to scope data to a particular user.</p>
</li>
</ol>
<blockquote>
<p>The repo is a fullstack example with <code>react-router-dom</code>. If you want to learn how to create a fullstack app with Clerk and <code>react-router-dom</code>, checkout the getting started page from <a target="_blank" href="https://clerk.com/docs/references/react/add-react-router">Clerk</a>.</p>
</blockquote>
<h2 id="heading-api-endpoints-with-clerk-and-aws-appsync">API Endpoints with Clerk and AWS AppSync</h2>
<p>If your goal is to have Clerk provide the authentication for an app where everyone signs in and shares data, then the rest is pretty straightforward:</p>
<p>In your Amplify Gen 2 schema, you can have Amplify create your resources as normal:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> schema = a.schema({
    Milestone: a
        .model({
            title: a.string().required(),
            description: a.string(),
        })
        .authorization(<span class="hljs-function">(<span class="hljs-params">allow</span>) =&gt;</span> [allow.publicApiKey(), allow.custom(<span class="hljs-string">'function'</span>)])
})
</code></pre>
<p>That will create the resolvers for you to be able to to create, read, update, and delete.</p>
<p>However, typically, you'll want to scope those operations so that only the logged in user can manipulate the data, without affecting everyone else. This is called <em>owner-based authorization</em>, and what I'll show.</p>
<blockquote>
<p>🗒️ The following JS resolvers work both with Amplify Gen 2 and the AWS CDK</p>
</blockquote>
<h3 id="heading-creating-an-item-with-owner">Creating an item with owner</h3>
<p>Imagine we already have a datasource: a DynamoDB table setup. Also, recall that our Lambda function used for authorization is passing the <code>owner</code> on the <code>resolverContext</code>.</p>
<pre><code class="lang-typescript"><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> * <span class="hljs-keyword">as</span> ddb <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-appsync/utils/dynamodb'</span>

<span class="hljs-comment">// This is the request to our database</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> { owner } = ctx.identity.resolverContext
    <span class="hljs-keyword">const</span> id = util.autoId()
    <span class="hljs-keyword">const</span> now = util.time.nowISO8601()
    <span class="hljs-keyword">const</span> item = {
        ...ctx.args,
        id,
        owner,
        createdAt: now,
        updatedAt: now,
    }
    <span class="hljs-keyword">const</span> key = { id }
    <span class="hljs-keyword">return</span> ddb.put({ key, item })
}

<span class="hljs-comment">// This is the response from our database</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>{
    <span class="hljs-keyword">return</span> ctx.result
}
</code></pre>
<p>Because AWS AppSync has first-class support for DynamoDB, we can make use of the AppSync <code>util</code> package, as well as the DynamoDB helpers package.</p>
<p>We combine those two to create things like a random <code>id</code>, set the time using <code>now.ISO8601</code>, and <code>put</code> the item in the table with its <code>owner</code> field.</p>
<p>The <code>result</code> is simply the item as it was stored in DynamoDB.</p>
<p>In short, any signed in user can create.</p>
<h3 id="heading-getting-an-item-by-owner">Getting an item by owner</h3>
<p>When it comes to getting an item from DynamoDB, we effectively say, "Any signed in use can get an item from the database, but for the database to respond back to the client with the information, the <code>owner</code> field on the item has to match the <code>owner</code> field from the Lambda authorizer:</p>
<pre><code class="lang-typescript"><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> * <span class="hljs-keyword">as</span> ddb <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-appsync/utils/dynamodb'</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">// get a todo by its id (API protects this so only auth users can call it)</span>
    <span class="hljs-keyword">return</span> ddb.get({ key: { id: ctx.args.id } })
}

<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-comment">//if the owner field isn't the same as the identity, the throw</span>
    <span class="hljs-keyword">const</span> { owner: clerkUser } = ctx.identity.resolverContext
    <span class="hljs-keyword">const</span> { owner: ddbOwner } = ctx.result
    <span class="hljs-keyword">if</span> (ddbOwner !== clerkUser) {
        util.unauthorized()
    }

    <span class="hljs-keyword">return</span> ctx.result
}
</code></pre>
<p>So long as that is the case, then return the data.</p>
<h3 id="heading-updating-an-item-if-owner">Updating an item if owner</h3>
<p>Updating is fun because now we get to use DynamoDB conditions to do a lot of the work for us:</p>
<pre><code class="lang-typescript"><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> * <span class="hljs-keyword">as</span> ddb <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-appsync/utils/dynamodb'</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> { id, title, description } = ctx.args
    <span class="hljs-keyword">const</span> { owner } = ctx.identity.resolverContext
    <span class="hljs-keyword">const</span> now = util.time.nowISO8601()

    <span class="hljs-keyword">const</span> updateObj = {
        title: ddb.operations.replace(title),
        description: ddb.operations.replace(description),
        updatedAt: ddb.operations.replace(now),
    }

    <span class="hljs-comment">// update it if owner.</span>
    <span class="hljs-keyword">return</span> ddb.update({
        key: { id },
        update: updateObj,
        condition: { owner: { eq: owner } },
    })
}

<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>Think of a form in a web app, were it populates all of the data about the item in various <code>input</code> boxes. The user can then update them and click <em>submit</em>. The backend get all of the information and simply updates the item in the database. What I love about updating is it shows the <code>replace</code> utility as well as the <code>time.nowISO8601</code> utility for updating the timestamp.</p>
<h3 id="heading-deleting-an-item-if-owner">Deleting an item if owner</h3>
<p>Deleting is similar to getting an item, except we get to rely on DynamoDB conditions to make sure it's only deleted is the <code>owner</code> fields match:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> ddb <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-appsync/utils/dynamodb'</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">// delete a todo by its id if it's the owner</span>
    <span class="hljs-keyword">const</span> { owner } = ctx.identity.resolverContext
    <span class="hljs-keyword">return</span> ddb.remove({
        key: { id: ctx.args.id },
        condition: { owner: { eq: owner } },
    })
}

<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>
<h3 id="heading-pagination-listing-items-if-owner">Pagination (listing items if owner)</h3>
<p>I saved this one for last because it's its own way of doing things. In a naïve implementation, we may be tempted to just return an array of items. In fact, if we were <code>scan</code>ning the table, that's what we'd get back. But in a real-world scenario, we want to efficiently query the data in our table and return a placeholder (<code>token</code>) if there are more items than our <code>limit</code> is set for:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> ddb <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-appsync/utils/dynamodb'</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> { owner } = ctx.identity.resolverContext
    <span class="hljs-keyword">return</span> ddb.query({
        query: { owner: { eq: owner } },
        index: <span class="hljs-string">'milestonesByOwner'</span>,
        limit: ctx.args.limit || <span class="hljs-number">25</span>,
        nextToken: ctx.args.nextToken,
    })
}

<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">const</span> { items = [], nextToken } = ctx.result

    <span class="hljs-keyword">return</span> { items, nextToken }
}
</code></pre>
<p>There's nothing inherently <em>hard</em> about this example, but it takes consideration. <code>limit</code> and <code>nextToken</code> aside, the <code>index</code> is what matters most here.</p>
<p>By default, our database is set with a <code>primaryKey</code> of <code>id</code>. However, for listing content, we want only search through items by their <code>owner</code> field. In short, we want a <code>globalSecondaryIndex</code>.</p>
<p>If not using Amplify and just using vanilla CDK, this can be achieved fairly easily as shown in my CDK starter repo. And if using Amplify Gen 2, this can be achieved as shown in the <code>amplify/data/resource.ts</code> file.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/fullstack-nextjs-cdk-starter/blob/main/_backend/lib/tables/dynamodb.ts#L17-L21">https://github.com/focusOtter/fullstack-nextjs-cdk-starter/blob/main/_backend/lib/tables/dynamodb.ts#L17-L21</a></div>
<p> </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That's all there is :) The repo has all of the methods used on the frontend so that you can practice making calls to the backend. If you enjoyed this tutorial and would like to see AWS integrated with other resources, let me know by dropping a comment!</p>
<p>Until next time, Happy Coding 🦦</p>
]]></content:encoded></item><item><title><![CDATA[How to Send Digital Products Easily Using Tally Forms and AWS Amplify Gen 2]]></title><description><![CDATA[Whether it's signing up to receive a free e-book, having folks register for an event, or any other of the many reasons, emailing your customers/users a product when they submit a form is a very common task. So why isn't it easier?
In this post, I'll ...]]></description><link>https://blog.focusotter.com/how-to-send-digital-products-easily-using-tally-forms-and-aws-amplify-gen-2</link><guid isPermaLink="true">https://blog.focusotter.com/how-to-send-digital-products-easily-using-tally-forms-and-aws-amplify-gen-2</guid><category><![CDATA[focusotter]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Startups]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[full stack]]></category><category><![CDATA[AWS Amplify]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Wed, 05 Jun 2024 21:53:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1717002211663/3b0153b0-c3c4-419c-a8a0-32f1ea894252.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Whether it's signing up to receive a free e-book, having folks register for an event, or any other of the many reasons, emailing your customers/users a product when they submit a form is a <em>very</em> common task. So why isn't it easier?</p>
<p>In this post, I'll walk through how to quickly get an application setup with Tally forms and extend it into the AWS ecosystem via webhooks. This will allow us to build out an app that can email a digital product to our users after it's fetched from an S3 bucket.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/RJudDFYmv84">https://youtu.be/RJudDFYmv84</a></div>
<p> </p>
<h2 id="heading-application-overview">Application Overview</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717006000394/5b74ebc3-61d8-435b-a95c-07659bb3ee0f.png" alt class="image--center mx-auto" /></p>
<p>The frontend is fairly simple. Essentially, give a form created by <a target="_blank" href="http://tally.so">Tally</a>, a user can fill it out and through the use of <a target="_blank" href="https://tally.so/help/webhooks">Tally webhooks</a>, our Lambda function can be triggered.</p>
<p>The backend is architected using <a target="_blank" href="https://docs.amplify.aws/">Amplify Gen 2</a> and simply creates a Lambda function, and an S3 bucket.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717006005664/f3632130-fa41-4405-8298-abdb8fab2332.png" alt class="image--center mx-auto" /></p>
<p>The repository for this project contains all of the code needed to get this working:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/tally-webhook-email-song">https://github.com/focusOtter/tally-webhook-email-song</a></div>
<p> </p>
<p>The core logic for this application is primarily to get the item from S3 as a presigned URL, and then to send an <code>&lt;a&gt;</code> tag with the <code>href</code> value set to the presigned URL:</p>
<pre><code class="lang-typescript">    <span class="hljs-comment">// Get the song url from S3</span>

    <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">await</span> generatePresignedURL(
        env.SONG_STORAGE_BUCKET_NAME,
        env.SONG_PATH,
        <span class="hljs-number">3600</span> <span class="hljs-comment">//expires in 1 hour</span>
    )
    <span class="hljs-comment">// Send email with the song</span>
    <span class="hljs-keyword">await</span> sendHTMLEmail(
        env.VERIFIED_SES_FROM_EMAIL,
        [email],
        <span class="hljs-string">'Your song has arrived!'</span>,
        <span class="hljs-string">`
        &lt;html&gt;
        &lt;body&gt;
        &lt;h1&gt;Hello from Focus Otter Music!&lt;/h1&gt;
        &lt;p&gt;Hey <span class="hljs-subst">${firstName}</span>, thanks so much for your purchase!&lt;/p&gt;
        &lt;p&gt;Here is &lt;a href="<span class="hljs-subst">${url}</span>"&gt;your song&lt;/a&gt;. Hope you enjoy!&lt;/p&gt;
        &lt;/body&gt;
        &lt;/html&gt;
    `</span>
    )
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This is a great starter project for those wanting to get into AWS. However, there are times where the file you'd like to send may be too large to embed in the body of the file. For those cases, you'll want to send it as an attachment. Fortunately, I have a blog post setup that walks through exactly how to do that as well!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://hashnode.com/post/clx2692tn000609kv06beccwa">https://hashnode.com/post/clx2692tn000609kv06beccwa</a></div>
]]></content:encoded></item><item><title><![CDATA[Easily Email Digital Products with Stripe and Amplify Gen 2]]></title><description><![CDATA[I'm a huge fan of cutting out the middle-man whenever possible. Don't get me wrong, there are some niceties that come with using a 3rd-party SaaS or wrapper service, but the tradeoff usually means cutting into profits, or working within the constrain...]]></description><link>https://blog.focusotter.com/easily-email-digital-products-with-stripe-and-amplify-gen-2</link><guid isPermaLink="true">https://blog.focusotter.com/easily-email-digital-products-with-stripe-and-amplify-gen-2</guid><category><![CDATA[AWS]]></category><category><![CDATA[Startups]]></category><category><![CDATA[serverless]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[AWS Amplify]]></category><category><![CDATA[focusotter]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Wed, 05 Jun 2024 18:38:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1717607364520/02d37e6c-7aed-48b1-ac8c-1ef4c6035e6a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm a huge fan of cutting out the middle-man whenever possible. Don't get me wrong, there are some niceties that come with using a 3rd-party SaaS or wrapper service, but the tradeoff usually means cutting into profits, or working within the constraints of their ecosystem.</p>
<p>For those reasons, in this post, I'll show how to use Stripe and AWS to create an application that allows users to buy a product and have it emailed to them!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/88T0-C1mKFU">https://youtu.be/88T0-C1mKFU</a></div>
<p> </p>
<h2 id="heading-application-overview">Application Overview</h2>
<p>All of the code for this project can be found here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/simple-stripe-checkout.git">https://github.com/focusOtter/simple-stripe-checkout.git</a></div>
<p> </p>
<p>The entry point of this application is based on a Stripe payment link that is triggered when a user clicks the "Get Your Song🎵" button.</p>
<p>Once a user makes a purchase, a Lambda function listens to the <code>checkout.session.completed</code> event type to perform code. To make sure only Stripe is authorized to process these events, the Stripe webhook signature is validated.</p>
<p>After validation, we can grab our song from Amazon S3 and email it to the customer as a file attachment. As opposed to an inline email, file attachments allow for larger file upload.</p>
<blockquote>
<p>🗒️ This setup assumes a verified email address. If you're SES account is in the sandbox, ensure a verified email is being used during development. The YouTube video has tips on getting out of the sandbox and moving to production.</p>
</blockquote>
<p>The entire application flow looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717608003130/e1503e66-1bbe-425d-afe6-c2810de9613b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-backend-setup">Backend Setup</h2>
<p>Using <a target="_blank" href="https://docs.amplify.aws/">AWS Amplify Gen 2</a>, I'm able to not only create my AWS services, but also have a full TypeScript experience. This means less errors and a simpler developer experience.</p>
<h3 id="heading-creating-the-amplify-backend">Creating the Amplify backend</h3>
<p>In previous versions of Amplify, a backend was scaffolded and iterated on using the Amplify CLI. In Gen 2, the CLI is only used as an entry point while the iteration happens via TypeScript:</p>
<pre><code class="lang-bash">npm create amplify@latest
</code></pre>
<p>This will create both a data resource (API + database) and an auth resource (defaults to email sign in).</p>
<p>In this application, we're using neither, so as you can see in the repo, I deleted those respective folders.</p>
<h3 id="heading-creating-an-s3-bucket">Creating an S3 bucket</h3>
<p>The S3 bucket needed to contain our MP3 needs to authorize our to-be-created Lambda function with <code>read</code> access. Fortunately, doing so is around 5 lines of code.</p>
<p>Using the GitHub repo as reference, I have following piece of code in the <code>amplify/storage/resource.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineStorage } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-amplify/backend'</span>
<span class="hljs-keyword">import</span> { stripeCheckoutSongFunc } <span class="hljs-keyword">from</span> <span class="hljs-string">'../functions/stripe-checkout/resource'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> storage = defineStorage({
    name: <span class="hljs-string">'amplifyStripeSongCheckout'</span>,
    access: <span class="hljs-function">(<span class="hljs-params">allow</span>) =&gt;</span> ({
        <span class="hljs-string">'products/*'</span>: [allow.resource(stripeCheckoutSongFunc).to([<span class="hljs-string">'read'</span>])],
    }),
})
</code></pre>
<h3 id="heading-creating-the-lambda-function">Creating the Lambda function</h3>
<p>The Lambda function needing to serve as the webhook for Stripe is similarly simple to setup. In the <code>amplify/functions/stripe-checkout</code> file I give it an <code>name</code>, an <code>entry</code> point for my business logic, and pass in a few <code>environment</code> variables:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineFunction, secret } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-amplify/backend'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> stripeCheckoutSongFunc = defineFunction({
    name: <span class="hljs-string">'stripe-checkout-song'</span>,
    entry: <span class="hljs-string">'./main.ts'</span>,
    environment: {
        <span class="hljs-comment">// STRIPE_SECRET: secret('stripe-secret'),</span>
        <span class="hljs-comment">// STRIPE_WEBHOOK_SECRET: secret('stripe-webhook'),</span>
        SONG_KEY: <span class="hljs-string">'products/amplify-song.mp3'</span>,
        FROM_EMAIL_ADDRESS: <span class="hljs-string">'mtliendo@focusotter.com'</span>,
    },
})
</code></pre>
<h3 id="heading-initial-sandbox-deploy">Initial sandbox deploy</h3>
<p>With our services scaffolded, now is a good time to perform an early deploy of our backend. Deploying early is a great way to catch problems in syntax before they cascade into bigger problems. Before doing so, we have to add these two services to our <code>backend</code> stack.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { stripeCheckoutSongFunc } <span class="hljs-keyword">from</span> <span class="hljs-string">'./functions/stripe-checkout/resource'</span>
<span class="hljs-keyword">import</span> { defineBackend } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-amplify/backend'</span>
<span class="hljs-keyword">import</span> { storage } <span class="hljs-keyword">from</span> <span class="hljs-string">'./storage/resource'</span>
<span class="hljs-keyword">import</span> { FunctionUrlAuthType } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-lambda'</span>
<span class="hljs-keyword">import</span> { PolicyStatement } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-iam'</span>

<span class="hljs-keyword">const</span> backend = defineBackend({
    storage,
    stripeCheckoutSongFunc,
})
</code></pre>
<p>In addition to adding the <code>storage</code> and <code>stripeCheckoutSongFunc</code> services that we created, we'll want to both generate a function URL that we can pass to Stripe, and provide permission for our function to use the SES service. To accomplish that, I added the following just below what's currently in that file. Once done, I can deploy my app to AWS:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> stripeWebhookUrlObj =
    backend.stripeCheckoutSongFunc.resources.lambda.addFunctionUrl({
        authType: FunctionUrlAuthType.NONE,
    })

backend.addOutput({
    custom: {
        stripeCheckoutSongFunc: stripeWebhookUrlObj.url,
    },
})

backend.stripeCheckoutSongFunc.resources.lambda.addToRolePolicy(
    <span class="hljs-keyword">new</span> PolicyStatement({
        actions: [<span class="hljs-string">'ses:SendEmail'</span>, <span class="hljs-string">'ses:SendRawEmail'</span>],
        resources: [<span class="hljs-string">'*'</span>],
    })
)
</code></pre>
<pre><code class="lang-bash">npx ampx sandbox
</code></pre>
<p>This long-running service will continuously watch for backend changes and automatically redeploy our application when those occur.</p>
<p>This is great so far, but the application doesn't make use of any secrets from Stripe. Let's fix that.</p>
<h3 id="heading-accessing-secret-values-from-stripe">Accessing secret values from Stripe</h3>
<p>This application needs 2 secret values: A stripe webhook secret, and the Stripe account secret. The secret key is pretty simple to gather, just make sure you don't share it!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717610599208/a76afe7e-9fa6-4353-9461-f8e7593237dc.png" alt class="image--center mx-auto" /></p>
<p>The webhook secret can only be given once a webhook has been entered, fortunately, by clicking on the "Webhooks" tab in Stripe, we can do just that:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717610700460/ba5385c1-da9d-41a1-9eb2-88e4e9480a4c.png" alt class="image--center mx-auto" /></p>
<p>Once complete, you'll want to grab the "signing secret" and make sure you never commit or share it!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717610780904/9291eeda-ca44-43e7-9f01-bf1be3868c38.png" alt class="image--center mx-auto" /></p>
<p>All that's left is to create your product and get a URL for the payment link. This can be done by clicking the <code>+</code> icon in the top-right, or simply pressing <code>c</code>, <code>l</code> on any page in Stripe(🤯).</p>
<p>I setup mine like this, but feel free to setup yours however you like!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717610898956/5021aceb-43b3-443a-93f8-eaebd1b9cb4a.png" alt class="image--center mx-auto" /></p>
<p>The payment link is what is put on the button on our landing page.</p>
<h3 id="heading-storing-secret-values">Storing Secret Values</h3>
<p>It's worth noting that the <code>secret</code> function provided by Amplify (as seen in our Lambda function resource) will take in a secret name and automatically handle grabbing the secret value. However, setting the secret in the first place is handled via the CLI:</p>
<pre><code class="lang-bash">npx ampx sandbox secret <span class="hljs-built_in">set</span> stripe-secret

<span class="hljs-comment"># enter the secret value:</span>
</code></pre>
<p>I ran that command for both the <code>stripe-secret</code> and the <code>stripe-webhook</code> values. Afterwards, make sure to uncomment the environment variables in the resource file.</p>
<h2 id="heading-handling-backend-logic">Handling Backend Logic</h2>
<p>With our resources created, and secrets added, it's time to setup the business logic for our Lambda function.</p>
<p>While the <code>main.ts</code> file is essentially the "glue" that puts the logic together, the logic is really divided into 3 sections:</p>
<ol>
<li><p>Verifying the Stripe signature</p>
</li>
<li><p>Getting the song from S3</p>
</li>
<li><p>Sending the email as an attachment</p>
</li>
</ol>
<h3 id="heading-verifying-a-stripe-signature">Verifying a Stripe signature</h3>
<p>As shown in <code>amplify/functions/stripe-checkout/helpers/verifyStripeWebhook.ts</code>, this function is generic and ready to be reused across applications:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> Stripe <span class="hljs-keyword">from</span> <span class="hljs-string">'stripe'</span>
<span class="hljs-keyword">import</span> { env } <span class="hljs-keyword">from</span> <span class="hljs-string">'$amplify/env/stripe-checkout-song'</span>

<span class="hljs-keyword">type</span> Event = {
    headers: {
        <span class="hljs-string">'stripe-signature'</span>: <span class="hljs-built_in">string</span>
    }
    body: <span class="hljs-built_in">string</span>
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> verifyWebhookSig = <span class="hljs-keyword">async</span> (event: Event) =&gt; {
    <span class="hljs-keyword">const</span> stripe = <span class="hljs-keyword">new</span> Stripe(env.STRIPE_SECRET)
    <span class="hljs-keyword">const</span> sig = event.headers[<span class="hljs-string">'stripe-signature'</span>]
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> stripeEvent = stripe.webhooks.constructEvent(
            event.body,
            sig,
            env.STRIPE_WEBHOOK_SECRET
        )
        <span class="hljs-keyword">return</span> stripeEvent
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'uh oh'</span>, err)
        <span class="hljs-keyword">throw</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid signature'</span>)
    }
}
</code></pre>
<p>It simply expects the Lambda <code>event</code> object and from there pulls off the headers while using the Stripe secret we created to perform the validation.</p>
<h3 id="heading-getting-an-object-from-s3">Getting an Object from S3</h3>
<p>This is another file that I templated so that it can be easily reused across projects:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { GetObjectCommand, GetObjectCommandOutput } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/client-s3'</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">const</span> s3Client = <span class="hljs-keyword">new</span> S3Client()

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getObjectFromS3</span>(<span class="hljs-params">{
    bucketName,
    objKey,
}: {
    bucketName: <span class="hljs-built_in">string</span>
    objKey: <span class="hljs-built_in">string</span>
}</span>) </span>{
    <span class="hljs-keyword">let</span> res: GetObjectCommandOutput | <span class="hljs-literal">undefined</span>
    <span class="hljs-keyword">try</span> {
        res = <span class="hljs-keyword">await</span> s3Client.send(
            <span class="hljs-keyword">new</span> GetObjectCommand({ Bucket: bucketName, Key: objKey })
        )
    } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'error getting object'</span>, e)
    }

    <span class="hljs-keyword">return</span> res
}
</code></pre>
<p>Essentially, you pass it the name of the S3 bucket, and the key of the object. Both of these items are stored as environment variables, making this a simple solution for grabbing any item from S3!</p>
<h3 id="heading-sending-the-email-as-an-attachment-with-ses">Sending the Email as an attachment with SES</h3>
<p>Lastly, and still in a reusable fashion, this we want the ability to send an email with both text, and an attachment. As opposed to sending an <code>html</code> formatted email, this involves dropping down to a <code>raw</code> email format:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { SESv2Client, SendEmailCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/client-sesv2'</span>
<span class="hljs-keyword">import</span> { env } <span class="hljs-keyword">from</span> <span class="hljs-string">'$amplify/env/stripe-checkout-song'</span>
<span class="hljs-keyword">const</span> sesClient = <span class="hljs-keyword">new</span> SESv2Client()

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> sendSESEmailWithAttachment = <span class="hljs-keyword">async</span> (
    fileBuffer: Buffer,
    toEmailAddresses: <span class="hljs-built_in">string</span>[],
    fileContentType: <span class="hljs-built_in">string</span>,
    fileName: <span class="hljs-built_in">string</span>
) =&gt; {
    <span class="hljs-keyword">const</span> myEmail = env.FROM_EMAIL_ADDRESS
    <span class="hljs-keyword">const</span> rawMessage = <span class="hljs-string">`From: <span class="hljs-subst">${myEmail}</span>
To: <span class="hljs-subst">${toEmailAddresses.join(<span class="hljs-string">', '</span>)}</span>
Subject: Email with MP3 Attachment
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="boundary_string"

--boundary_string
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit

This email has an MP3 attachment.

--boundary_string
Content-Type: <span class="hljs-subst">${fileContentType}</span>
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="<span class="hljs-subst">${fileName}</span>"

<span class="hljs-subst">${fileBuffer.toString(<span class="hljs-string">'base64'</span>)}</span>
--boundary_string--`</span>

    <span class="hljs-keyword">const</span> sendEmailParams = {
        Content: {
            Raw: {
                Data: Buffer.from(rawMessage),
            },
        },
    }

    <span class="hljs-keyword">const</span> sendEmailCommand = <span class="hljs-keyword">new</span> SendEmailCommand(sendEmailParams)

    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> sesClient.send(sendEmailCommand)
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Email sent successfully. Message ID: <span class="hljs-subst">${result.MessageId}</span>`</span>)
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error sending email:'</span>, err)
    }
}
</code></pre>
<blockquote>
<p>🗒️ It's ugly. But I think it's just the long string that makes it look bad.</p>
</blockquote>
<h3 id="heading-conclusion">Conclusion</h3>
<p>With those 3 pieces in place, the <code>amplify/functions/stripe-checkout/main.ts</code> file simply makes use of them to create our application flow.</p>
<p>If wanting to clean up and delete the resources, running <code>npx ampx sandbox delete</code> will do just that (or cancelling out of the running sandbox will trigger a prompt). Also worth noting is that any secrets we added in the sandbox must be manually removed with <code>npx ampx sandbox secret remove [secret-name]</code>.</p>
<p>What I love about this project is that it can be reused anytime you want to sell <em>any</em> digital item online! Making it a super fast an easy solution!</p>
<p>Hope this helped and if you enjoyed this piece let me know by dropping a comment!</p>
<p>Until next time, Happy Coding 🦦</p>
]]></content:encoded></item><item><title><![CDATA[Fullstack Solutions: AWS Amplify Gen 2 vs AWS CDK]]></title><description><![CDATA[I'm sitting in a Seattle pub writing this post and I'm having a hard time conentrating because my Airpods died. It's my fault for not charging them, but I wish I had wired headphones that I could plug in. They would just work™️. I wouldn't have to wo...]]></description><link>https://blog.focusotter.com/fullstack-solutions-aws-amplify-gen-2-vs-aws-cdk</link><guid isPermaLink="true">https://blog.focusotter.com/fullstack-solutions-aws-amplify-gen-2-vs-aws-cdk</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws-cdk]]></category><category><![CDATA[AWS Amplify]]></category><category><![CDATA[focusotter]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[full stack]]></category><category><![CDATA[Startups]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Wed, 22 May 2024 19:29:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716342547182/f54d9d40-c547-4c1c-9df3-c4ad72903a3e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm sitting in a Seattle pub writing this post and I'm having a hard time conentrating because my Airpods died. It's my fault for not charging them, but I wish I had wired headphones that I could plug in. They would just work™️. I wouldn't have to worry about one earbud losing power, and the initial setup just involves taking them out of the box and plugging them in.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716342958719/2e51aa4a-ecd3-4c42-a6c0-ef46c3b11d8d.jpeg" alt class="image--center mx-auto" /></p>
<p>Yet, there's something to love about the flexibility of putting in just one earbud, or exercising without getting tangled in wire, or even the different customizations I can make with pressing, long-pressing, sliding etc.</p>
<p>Maximum ease-of-use vs maximum flexibility.</p>
<p>In case it's not apparent, that's how I felt when comparing <a target="_blank" href="https://docs.amplify.aws/">AWS Amplify Gen 2</a> vs my custom AWS solution with the <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/">AWS CDK</a> when it came to building fullstack apps.</p>
<p>In this post, I'll discuss some of the similarities between these solutions, their differences, and when I would use one over the other.</p>
<blockquote>
<p>🗒️ While this post is intended for fullstack developers, It slants towards those with knowledge of the AWS CDK. If coming from the other end and are more of a frontend developer, I encourage you to read my <a target="_blank" href="https://hashnode.com/post/clwfaegar000i09l93k396ejc">last post</a>.</p>
</blockquote>
<h2 id="heading-aws-cdk-for-power-and-flexibility">AWS CDK for Power and Flexibility</h2>
<p>When your next great idea strikes for a fullstack application, there are plenty of times you'll want to reach for the AWS CDK directly. This wrapper around AWS Cloudformation gives developers the freedom to write their backend in TypeScript, Python, Java, and other languages. This can be a huge benefit and unblocker in itself for developers and teams.</p>
<h3 id="heading-surface-level">Surface Level</h3>
<p>Creating a new fullstack application, where the backend is powered by the AWS CDK is <em>powerful</em> and simple:</p>
<pre><code class="lang-bash">npm create vite@latest ./ &amp;&amp; \
md _backend &amp;&amp; \
<span class="hljs-built_in">cd</span> <span class="hljs-variable">$_</span> &amp;&amp; \
npx aws-cdk init -l typescript \
</code></pre>
<p>If you need authentication, an API, a database, file storage, etc, you get to craft those exactly how you want.</p>
<p>Setting up a team-based workflow is also great because you get to specify exactly how you want your workflow to be and what the day-to-day experience will be like.</p>
<p>And when it comes to deployment, you get to have full control on how you'd like it deployed.</p>
<p>These are all great! Personally, from a learning perspective, it also meant I got to understand services at a much more clarified level than using a tool that generated it for me. It helps that I'm never really <em>extending</em> the CDK since I'm working at more or less the same level the whole time. I'm simply building.</p>
<h3 id="heading-diving-deeper">Diving Deeper</h3>
<p>However, if you re-read the section above and replace each instance of "you get to" with "you have to". Then things start to become much more daunting.</p>
<blockquote>
<p>🗒️ It's worth noting that there is a tool called <a target="_blank" href="https://github.com/projen/projen">Projen</a> that aims to simplify some of the opinions needed to be made. You can learn more about the <a target="_blank" href="https://aws.amazon.com/blogs/devops/getting-started-with-projen-and-aws-cdk/">Pros and Cons</a> of that package here.</p>
</blockquote>
<p>For this reason, I created my own CDK starter repo. It comes with many of the configuration options that I like and simplifies some of the mundane work needed to get my fullstack idea off the ground.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/fullstack-nextjs-cdk-starter">https://github.com/focusOtter/fullstack-nextjs-cdk-starter</a></div>
<p> </p>
<p>This worked out well because I often build fullstack, monorepo applications. It also comes with basic CRUD operations for my API so I can simply reference them and tweak them accordingly.</p>
<blockquote>
<p>At the expense of sounding blasphemous, this was my attempt at creating Amplify Gen 2 before it existed 😅</p>
</blockquote>
<p>This is just a personal project though and not something that I guarantee will work for you (I even say so in the readme!).</p>
<p>With that said, while the repo I created uses <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-amplify-alpha-readme.html">a CDK construct</a> to deploy a frontend to AWS Amplify, maybe that's not what you want. Perhaps you want to keep your frontend and backend separated in different GitHub repos. That's to say, everytime a backend change occurs, a GitHub action will try to build and deploy your changes to AWS.</p>
<p>That's what I built with my GitHub OIDC repo:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/github-aws-oidc-provider-cdk">https://github.com/focusOtter/github-aws-oidc-provider-cdk</a></div>
<p> </p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>I say all of this to highlight the flexibility, and power in setting this up exactly how you want. However, it's time consuming. If you're in a large organization, you've likely already invested the time into this and have something similar to what I created above.</p>
<p>Though, if you are not a large organization the above is a lot of work and probably enough to say "nevermind". At the end of the day, choosing to understand the tools you use will serve you in the long run--greatly impacting the overall <a target="_blank" href="https://en.wikipedia.org/wiki/Total_cost_of_ownership">Total Cost of Ownership</a>.</p>
<h2 id="heading-aws-amplify-for-speed-and-ease-of-use">AWS Amplify for Speed and Ease of Use</h2>
<p>As the AWS CDK wraps Cloudformation so that customers don't have to write extraneous amounts of YAML, AWS Amplify wraps the CDK to provide structure for building fullstack apps*.</p>
<p>I mentioned in a recent post, <a target="_blank" href="https://hashnode.com/post/clwfaegar000i09l93k396ejc">AWS Amplify in 2024 is not the Amplify you grew up with</a>. Gone are the days of writing a GraphQL schema, or using predefined storage paths like <code>private</code> , <code>protected</code> , and <code>public</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Setting up a data model in Gen 2 using TypeScript</span>

<span class="hljs-keyword">const</span> schema = a.schema({
    Todo: a
        .model({
            content: a.string(),
        })
        .authorization(<span class="hljs-function">(<span class="hljs-params">allow</span>) =&gt;</span> [allow.guest()]),
})
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// Setting up an S3 Bucket with authorization access in Gen 2</span>

<span class="hljs-keyword">const</span> storage = defineStorage({
  name: <span class="hljs-string">'myProjectFiles'</span>,
  access: <span class="hljs-function">(<span class="hljs-params">allow</span>) =&gt;</span> ({
    <span class="hljs-string">'media/*'</span>: [
      allow.guest.to([<span class="hljs-string">'read'</span>]) 
    ]
  })
})
</code></pre>
<p>In fact, in that last post, I showed how to extend the Amplify service so that custom API operations can be made instead of using the <code>.model</code> helper provided by Amplify.</p>
<h3 id="heading-surface-level-1">Surface Level</h3>
<p>Creating an application using Amplify is similarly simple to the CDK:</p>
<pre><code class="lang-bash">npm create vite@latest\
npm create amplify@latest\
</code></pre>
<p>The above is all that is needed to create a fullstack application that is powered by AWS and fully developed in TypeScript.</p>
<p>However, I'd be amiss if I didn't point out that TypeScript being used here isn't a personal preference, it's a requirement as opposed to the AWS CDK. This underscores Amplify being targeted towards fullstack teams and developers (more on this in a bit).</p>
<hr />
<p>If you are a solo developer, you might have one AWS account. If you're serious dev or a small team, you might have SSO setup. If you're a large org, you probably give every developer their own AWS account. These are just a few ways that day-to-day development is done.</p>
<p>In Amplify Gen 2, none of that matters because every project is developed in its own sandbox:</p>
<pre><code class="lang-bash">npx ampx sandbox [--profile=focus-otter]
</code></pre>
<p>Gone are the days of stepping on each others workflow. Instead, running the command above will not only do a <code>synth</code> and <code>deploy</code> of your application, but it will also generate the appropriate <code>CDKOutputs</code> and put the backend in <code>watch</code> mode.</p>
<p>When it comes to customizations from the basic CRUD operations that Amplify provides, in Amplify Gen 2 extensibility works differently. Gen 2 resources are file-based instead of CLI-based, so adding custom resources means just writing CDK code.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716387296146/de575f0e-7b5b-43f6-b363-d8014ce5ea12.png" alt class="image--center mx-auto" /></p>
<p>In the code above, notice how I'm using CDK L2 constructs in my application. Also, take notice of how the <code>type</code> of my <code>myCustomCDKStack</code> item is of type <code>Stack</code>.</p>
<p>Not some Amplify wrapped service. Just a plain 'ol CDK stack!</p>
<h3 id="heading-diving-deeper-1">Diving Deeper</h3>
<p>The <code>.model</code> part of the data resource will create an AppSync API, a DynamoDB database, associate the necessary roles/permissions, and generate all of the CRUDL operations + subscriptions, and their types.</p>
<p>That's a lot! But is it too much? I personally don't think so. Doing so by hand is boring and mundane. Every application needs those <em>capabilities</em>, yes. Though if you aren't a fan of those particular services...don't use them 🤷‍♂️</p>
<p>What I mean is Amplify doesn't bind developers to those services, but does take the opinion that most developers will benefit from this experience. To prove that, compare my CDK starter repo above, with my Amplify Gen2 starter repo here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/fullstack-gen2-starter">https://github.com/focusOtter/fullstack-gen2-starter</a></div>
<p> </p>
<p>My personal Gen 2 starter (not affiliated with the official Amplify Gen 2 starter) just works, comes integrated with it's own sandbox environment instead of me <a target="_blank" href="https://github.com/focusOtter/fullstack-nextjs-cdk-starter/blob/main/_backend/cdk.context.json">trying to create my own</a>, and if at any point I want to add more features, I simply drop down to using the CDK.</p>
<p>Amplify is doing for the CDK community what Expo did for React Native.</p>
<blockquote>
<p>"But Focus Otter, I don't want to have my backend and frontend bundled together."</p>
</blockquote>
<p>Great! Amplify Gen 2 no longer has to be tied to one--in fact, it only cares that your backend is written in TypeScript. What you use it for is completely up to you. This unlocks a new category of use cases that is not only <a target="_blank" href="https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/mono-and-multi-repos/">documented</a>, but officially supported by the Amplify team.</p>
<p>More on this feature in a future post!</p>
<p>In Amplify Gen 1, I recall having to do things "the Amplify way". There is still some of that, but as someone who has grown fond of developing with the CDK, doing things the Amplify way feels less like a shove in a direction, and more like a guided hand.</p>
<h2 id="heading-which-to-use">Which to use</h2>
<p>It would be too easy to write "it depends" and call it a day. So let me offer some practical advice for building fullstack applications on AWS:</p>
<p>Start with Amplify.</p>
<p>My headphone analogy in the beginning falls apart when I consider how deeply integrated Amplify Gen 2 is with the AWS CDK.</p>
<p>Sure, if you don't know JavaScript/TypeScript, then it's not a good fit. Full stop.</p>
<p>If you're Lambda functions aren't in TypeScript, <a target="_blank" href="https://github.com/aws-amplify/amplify-backend/issues/1543">stay tuned</a>.</p>
<p>Remember, that Amplify Hosting <em>integrates</em> with Amplify Gen 2, but they are separate. So if you have your own set of hosting requirements, you can still build with Gen 2.</p>
<p>I can't stress how critical I've been of this new workflow since testing it out internally--and even now. But it's good, really good. Yes, you can spend time coming up with the solutions using the CDK like how I did, but your time is better spent.</p>
<hr />
<p>Keep in mind, this post is for the masses. That's to say it's not a silver bullet but rather my current opinion on what should be your goto.</p>
<p>That's enough though leadership for me, for the next few weeks I'll be showing a bunch of fun applications that can be built with Amplify Gen 2!</p>
<p>Until then, happy coding 🦦</p>
]]></content:encoded></item><item><title><![CDATA[AWS Amplify in 2024 is not the Amplify you grew up with]]></title><description><![CDATA[In 2019 I got into learning AWS as a software engineer at John Deere. I took a course on Frontend Masters taught by Steve Kinney about how frontend developers could use a tool called AWS Mobile Hub.
Through the lens of Mobile Hub, I learned the found...]]></description><link>https://blog.focusotter.com/aws-amplify-in-2024-is-not-the-amplify-you-grew-up-with</link><guid isPermaLink="true">https://blog.focusotter.com/aws-amplify-in-2024-is-not-the-amplify-you-grew-up-with</guid><category><![CDATA[AWS Amplify]]></category><category><![CDATA[fullstack]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[focusotter]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Mon, 20 May 2024 18:15:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716228841311/fb992f21-9cdb-43de-8d5b-81aa8bae70fb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In 2019 I got into learning AWS as a software engineer at John Deere. I took a course on Frontend Masters taught by <a target="_blank" href="https://twitter.com/stevekinney">Steve Kinney</a> about how frontend developers could use a tool called AWS Mobile Hub.</p>
<p>Through the lens of Mobile Hub, I learned the foundations of AWS and serverless. However, the tool was buggy, soon deprecated, and shortly after, <a target="_blank" href="https://docs.amplify.aws/">AWS Amplify</a> came to be.</p>
<p>Through this tool, I didn't have to setup Cloudfront or S3. As a frontend/fullstack developer, I wanted to use something like what <a target="_blank" href="https://vercel.com/blog/zeit-is-now-vercel">Zeit (now Vercel)</a>, or Netlify had to offer. AWS Amplify Hosting allowed that.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=ucVK6Z55PZY&amp;t=1s">https://www.youtube.com/watch?v=ucVK6Z55PZY&amp;t=1s</a></div>
<p> </p>
<p>In addition, the Amplify CLI meant I didn't have to <em>know</em> AWS. I could just use the CLI. From easy use cases where I wanted an S3 bucket:</p>
<pre><code class="lang-bash">amplify add storage
</code></pre>
<p>To more complex integrations likes creating a GraphQL API with a database and authentication rules:</p>
<pre><code class="lang-bash">amplify add api
</code></pre>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Todo <span class="hljs-meta">@model</span> <span class="hljs-meta">@auth</span>(<span class="hljs-symbol">rules:</span> [{<span class="hljs-symbol">allow:</span> private}]) {
    <span class="hljs-symbol">id:</span> ID!
    <span class="hljs-symbol">name:</span> String!
}
</code></pre>
<p>I felt empowered to build <em>anything</em>.</p>
<p>Through the <a target="_blank" href="https://aws.amazon.com/developer/community/community-builders/">AWS Community Builders</a> program, I was able to get direct feedback to and from the AWS Amplify team until the Spring of 2021 when I was hired as a developer advocate on the AWS Amplify team.</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/focusotter/status/1398046515189399554">https://twitter.com/focusotter/status/1398046515189399554</a></div>
<p> </p>
<h2 id="heading-learning-how-to-da-and-icarus">Learning How to DA and Icarus</h2>
<p>As a developer advocate for the Amplify team I loved challenging myself to see how I could build out seemingly-complex applications using the Amplify CLI. I wasn't interested in todo-style or simple CRUD-type applications.</p>
<p>I instead wanted to build social media sites like Instagram, apps that have to pass data to one another like Starbucks, and multi-tenant SaaS applications like Slack.</p>
<p>I would often hit a roadblock, talk to the engineering team, find a workaround, and keep going forward. It wasn't that what I was doing couldn't be done, but there were times when the solution was so far-fetched that I couldn't believe that was the best form of DX we could have.</p>
<p>Naturally, I gave feedback and as expected, the Amplify team delivered. In 2022, the team announced <a target="_blank" href="https://aws.amazon.com/blogs/mobile/use-aws-cdk-v2-with-the-aws-amplify-cli-extensibility-features-beta/"><em>extensibility</em> with AWS Amplify</a>. This feature allowed customers to leverage the AWS CDK to provision services that the Amplify CLI didn't support.</p>
<blockquote>
<p>🗒️ This was absolutely the right move. AWS has over 200 services. It'd be ridiculous to have a CLI tool that had all of those services baked in!</p>
</blockquote>
<p>Through extensibility, I felt I had better control. The ceiling to what I could build as a sole developer was certainly raised and to this day, I'd say the majority of solo devs could use this and be profitable in both revenue and usage.</p>
<p>The problem was that I -- a solo developer -- had to know:</p>
<ul>
<li><p>A frontend framework</p>
</li>
<li><p>AWS Amplify's way of doing things (CLI, libraries, and patterns)</p>
</li>
<li><p>AWS CDK</p>
</li>
<li><p>TypeScript</p>
</li>
</ul>
<p>Not just that but what often gets overlooked, is knowing <em>when</em> to use those tools.</p>
<p>Looking back, I put out a lot of tutorials that talked about how to build Complicated App™️ and the solution was filled with various workarounds to what should have been a simple process.</p>
<p>I forgot that most developers don't have direct access to an AWS engineering team. That most don't get a perpetually free AWS account to work in. And that most don't know when an enhanced experience is just around the corner so they shouldn't invest in hackySolution™️.</p>
<blockquote>
<p>🗒️ One of my greatest realizations at AWS was when the director of our org spoke on the value of DA's by saying, "They [engineers] often will perform the workarounds without realizing that they're workarounds."</p>
<p>Nothing against our amazing engineers of course. His statement was simply about the impact of DA's being an important link the chain. Between our customers and product.</p>
</blockquote>
<p>I had become <a target="_blank" href="https://en.wikipedia.org/wiki/Icarus">Icarus</a>. Through my various solutions and content pieces, I flew high without realizing I was advocating for the product instead of the developers I was trying to help. And with each YouTube video that showed what was possible, my wings melted in the sun of what I thought was good enough.</p>
<h2 id="heading-switching-teams-and-going-in-on-aws">Switching teams and going in on AWS</h2>
<p>Soon after, my personal interests shifted towards learning AWS services and I switched to the AWS AppSync team.</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/focusotter/status/1570574512395255808">https://twitter.com/focusotter/status/1570574512395255808</a></div>
<p> </p>
<p>This is where I learned how to use the AWS CDK. I no longer had the luxury (and it is a luxury!) of letting Amplify do all the work in creating an API, adding authentication, or configuring a database. I also had to learn how to connect my frontend (NextJS) with my CDK backend.</p>
<p>It took months of learning and I'm fortunate my manager trusted me to figure out a DX that met my high bar.</p>
<p>I finally settled on a solution that worked well for the majority of small to medium-sized applications, and provided enough inspiration for medium to large applications.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/fullstack-nextjs-cdk-starter">https://github.com/focusOtter/fullstack-nextjs-cdk-starter</a></div>
<p> </p>
<p>My pitch to fullstack cloud developers was, "I'm trying to make your life 15% harder for unlimited flexibility".</p>
<p>Looking back, that pitch was pretty awful 😅</p>
<p>For context, this was when Supabase, SST, Vercel, and tRPC captivated crowds on what an amazing DX could look like. My solution works -- and many customers find success/inspiration from it, but it was a heavy sell for those outside of the niche.</p>
<p>So there I was. A DA no longer directly tied to the Amplify team or it's crowd of frontend developers. However, in building out fullstack applications, I wanted more flexibility than what Amplify had at the time, and I wanted frontend developers to have an easier onboarding than what the AWS CDK offers.</p>
<blockquote>
<p>🗒️ I hate to keep stressing this, but the Amplify CLI can get you <em>really</em> far. And the AWS CDK is my favorite IaC tool.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/playlist?list=PLiLHsu3XjGfMgGxbjK59tgGt0NYywpcDd">https://www.youtube.com/playlist?list=PLiLHsu3XjGfMgGxbjK59tgGt0NYywpcDd</a></div>
<p> </p>
</blockquote>
<h2 id="heading-enter-amplify-gen2">Enter Amplify Gen2</h2>
<p>The amazing thing about AWS is that our teams (the ones I interact with) are structured like a startup -- albeit a well funded one. What I mean is that we never really rest on <em>good enough</em>. We are always challenging each other and <a target="_blank" href="https://www.amazon.jobs/content/en/our-workplace/leadership-principles">Thinking Big</a>.</p>
<p>So when my colleague and mentor <a target="_blank" href="https://twitter.com/renebrandel">René Brandel</a> asked for my feedback on an API concept for Amplify, I took a look not expecting much of it.</p>
<p>One of our personal rules is that we don't do "company speak" or put fluff between the lines. We just say what we feel. So if something sucks, then we say it sucks and not stumble for the politically correct way of saying it. I encourage everyone to have a friend like this at work.</p>
<p>His solution sucked.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> ClientSchema, a, defineData } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-amplify/backend'</span>

<span class="hljs-comment">// a wordsearch API</span>
<span class="hljs-keyword">const</span> schema = a.schema({
    WordSearch: a
        .model({ <span class="hljs-comment">// a database</span>
            id: a.id().required(),
            name: a.string().required(),
            columns: a.integer().required(),
            rows: a.integer().required(),
            wordBank: a.string().array().required(),
        })
        .authorization([a.allow.owner()]), <span class="hljs-comment">// an auth rule</span>
})
</code></pre>
<p>It was all TypeScript based. There was no clear separation for when the frontend started and the backend began. The API was completely changed from what I was familiar with, and his presentation was filled with "what ifs", and "now imagine if you could"s.</p>
<p>I gave him my feedback on there being too much magic, how it changed how things were done, and how the problems customers faced when building applications would be the same and moved on with my day.</p>
<p>A few days later we met again to cover some minor changes he made. I remember telling him, "The API just doesn't look or <em>feel</em> like GraphQL". I'll never forget his reply:</p>
<p><strong>"Who said this is GraphQL?"</strong></p>
<p>Granted, it <strong><em>was</em></strong> GraphQL -- AppSync specifically. I knew that. But I only knew it because I was close to the product/engineering team. I realized I was being an Icarus -- flying high advocating for a product instead of what fullstack developers want.</p>
<blockquote>
<p>🗒️ In hindsight, his idea didn't suck. I just couldn't hit pause on what I was familiar with long enough to see the benefits of what was in front of me.</p>
</blockquote>
<p>He sent an early build for me to play around with and provide more feedback on, and this was in fact our cycle for the next several months.</p>
<p>In the end, thanks to him and the his team's engineering efforts, Amplify Gen 2 (preview) was announced during the re:invent 2023 season.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://aws.amazon.com/blogs/mobile/introducing-amplify-gen2/">https://aws.amazon.com/blogs/mobile/introducing-amplify-gen2/</a></div>
<p> </p>
<p>Admittedly, it wasn't until recently that I began trying to move my NextJS + CDK applications over to Amplify Gen 2. Most recently, <a target="_blank" href="https://github.com/focusOtter/sample-wordsearch">this</a> wordsearch app built with the CDK being ported over to this one using Gen 2 :</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/wordsearch-gen2">https://github.com/focusOtter/wordsearch-gen2</a></div>
<p> </p>
<h2 id="heading-amplify-gen-2-takeaways">Amplify Gen 2 Takeaways</h2>
<p>The rule I have with René is the same rule I have with all of you -- no fluff.</p>
<p>Amplify Gen 2 is nice...not perfect...but <em>very</em> nice!</p>
<h3 id="heading-frontend-developers">Frontend Developers</h3>
<p>From a frontend DX perspective, everything is in TypeScript. If you want to setup authentication, you do so using TypeScript. If you want to create an data resource (API), it's done in TypeScript as shown above.</p>
<p>This end-to-end type-safety is visible throughout the application. In a NextJS application for example, all of your CRUD operations are now fully-typed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712725690739/446d9d13-b291-474a-bfb5-65aa76fdd485.png" alt class="image--center mx-auto" /></p>
<p>This means less context-switching and mental overhead. In 2024, it's all but expected, but a nice DX with nonetheless.</p>
<h3 id="heading-backend-developers">Backend Developers</h3>
<p>For backend developers, the DX is even better IMO.</p>
<p>This may have flown under the radar for most people, but a big PR was merged in the CDK last year that the Amplify team was able to capitalize on: Making GraphQL schemas hot-swappable.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/aws/aws-cdk/pull/27197">https://github.com/aws/aws-cdk/pull/27197</a></div>
<p> </p>
<p>This means whenever a schema is changed, the running process would redeploy those assets to AWS. In relatable terms, this means when a customer runs the following command:</p>
<pre><code class="lang-bash">npx amplify sandbox
</code></pre>
<p>Then a stack (completely separated from your team/org) will get deployed and this process will watch for changes. When Amplify detects a change to your data file, for example, then it will redeploy your API, and run codegen on your behalf so that your get live feedback across your application.</p>
<blockquote>
<p>🗒️ Much like the <code>cdk watch</code> command, sandbox environments are used during development.</p>
</blockquote>
<hr />
<p>Simple CRUD apps are great to showcase because they cover common scenarios. However, it's important to remember real-world applications are often much more involved. For example, adding AI generated content often needs another service or custom ability.</p>
<p>In the CDK, a custom mutation that called out to <a target="_blank" href="https://aws.amazon.com/bedrock/?gclid=Cj0KCQjwztOwBhD7ARIsAPDKnkB6On-TOqL6nKQ-fSdIA1Q_dNgEdNdDTX7Ok12EshM0CCfz5la2g1kaAvwEEALw_wcB&amp;trk=0eaabb80-ee46-4e73-94ae-368ffb759b62&amp;sc_channel=ps&amp;ef_id=Cj0KCQjwztOwBhD7ARIsAPDKnkB6On-TOqL6nKQ-fSdIA1Q_dNgEdNdDTX7Ok12EshM0CCfz5la2g1kaAvwEEALw_wcB:G:s&amp;s_kwcid=AL!4422!3!692006004688!p!!g!!bedrock!21048268554!159639952935">Amazon Bedrock</a> would look like this:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Mutation {
    generateWordSearchWords(<span class="hljs-symbol">theme:</span> String!): String!
}
</code></pre>
<p>While simple at first, it's important to remember you still have to create the HTTP datasource:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// add bedrock as a datasource</span>
<span class="hljs-keyword">const</span> bedrockDataSource = api.addHttpDataSource(
    <span class="hljs-string">'bedrockDS'</span>,
    <span class="hljs-string">'https://bedrock-runtime.us-east-1.amazonaws.com'</span>,
    {
        authorizationConfig: {
            signingRegion: <span class="hljs-string">'us-east-1'</span>,
            signingServiceName: <span class="hljs-string">'bedrock'</span>,
        },
    }
)
</code></pre>
<p>Then give the datasource permissions to invoke the foundation model (Claude V2 in this case):</p>
<pre><code class="lang-typescript">
<span class="hljs-comment">// Allow datasource to invoke claude</span>
bedrockDataSource.grantPrincipal.addToPrincipalPolicy(
    <span class="hljs-keyword">new</span> PolicyStatement({
        resources: [
            <span class="hljs-string">'arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2'</span>,
        ],
        actions: [<span class="hljs-string">'bedrock:InvokeModel'</span>],
    })
)
</code></pre>
<p>Create and configure the custom resolver for your API:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// create a unit resolver that connects to bedrock and returns a string of words</span>
<span class="hljs-keyword">const</span> generateWordSearchWordsResolver = api.addResolver(
    <span class="hljs-string">'generateWordSearchWordsResolver'</span>,
    {
        dataSource: bedrockDataSource,
        typeName: <span class="hljs-string">'Mutation'</span>,
        fieldName: <span class="hljs-string">'generateWordSearchWords'</span>,
        code: Code.fromAsset(path.join(__dirname, <span class="hljs-string">'generateWordSearchWords.js'</span>)),
        runtime: FunctionRuntime.JS_1_0_0,
    }
)
</code></pre>
<p>And only then can you write your business logic for your application:</p>
<pre><code class="lang-typescript"><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> assistant = <span class="hljs-string">``</span>
    <span class="hljs-keyword">const</span> theme = ctx.args.theme
    <span class="hljs-keyword">const</span> prompt = <span class="hljs-string">`Generate 10 words related to <span class="hljs-subst">${theme}</span>. Put the words in an array. For example, if the theme was "animals", you would return ["monkey", "tiger", "bear", "lion", "gorilla", "bird", "penguin", "dolphin", "wolf", "dog"].`</span>

    <span class="hljs-keyword">return</span> {
        resourcePath: <span class="hljs-string">'/model/anthropic.claude-v2/invoke'</span>,
        method: <span class="hljs-string">'POST'</span>,
        params: {
            headers: {
                <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
            },
            body: {
                prompt: <span class="hljs-string">`\n\nHuman:<span class="hljs-subst">${prompt}</span>\n\nAssistant:<span class="hljs-subst">${assistant}</span>`</span>,
                max_tokens_to_sample: <span class="hljs-number">300</span>,
                temperature: <span class="hljs-number">0.5</span>,
                top_k: <span class="hljs-number">250</span>,
                top_p: <span class="hljs-number">1</span>,
                stop_sequences: [<span class="hljs-string">'\\n\\nHuman:'</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>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'the bedrock response'</span>, ctx.result.body)
    <span class="hljs-keyword">return</span> ctx.result.body
}
</code></pre>
<p>In a TypeScript environment powered by Amplify Gen 2, this becomes much easier:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712727156756/af553ee6-b76f-4219-afde-3a2a78d1d435.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>🗒️ You still have to <a target="_blank" href="https://github.com/focusOtter/wordsearch-gen2/blob/main/amplify/backend.ts">assign the correct permissions</a>, and your custom business logic is still up to you to decide.</p>
</blockquote>
<p>What I love about this model isn't just the type-safety, but that I am able to copy over the permissions from the CDK project because at the end of the day, it's all TypeScript.</p>
<p>I love frontend frameworks that use APIs and concepts from browsers and push developers to <a target="_blank" href="https://developer.mozilla.org/en-US/">MDN</a>. Amplify does the same by taking care of the expected and telling customers that want additional services to view the <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/">CDK docs</a>.</p>
<h3 id="heading-continuity">Continuity</h3>
<p>Amplify Gen 2 in your CLI and code editor also brings enhancements to the Hosting platform as well! For starters, it's now possible to have <a target="_blank" href="https://aws.amazon.com/blogs/mobile/wildcard-subdomains-for-multi-tenant-apps-on-aws-amplify-hosting/">wildcard subdomains</a> on your domain name. This, along with enhanced server-side rendering support means it's now possible to have true multi-tenant applications hosted on Amplify.</p>
<p>Additionally, because of the closer connection to the CDK, Amplify will <code>cdk bootstrap</code> your account/region on your behalf!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712728250591/e9b82fe4-2f68-4e9c-9606-41e5d03c08c2.png" alt class="image--center mx-auto" /></p>
<p>Also, for teams that may incrementally adopt Amplify, have a large team, or have an existing CDK backend, it's possible to keep the applications separated while taking advantage of all the Amplify goodness.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/mono-and-multi-repos/">https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/mono-and-multi-repos/</a></div>
<p> </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>A major part of developer advocacy is what <a target="_blank" href="https://twitter.com/BillFine">my manager</a> calls "selling what's on the truck". Meaning there are times when we know a big feature release is coming, but we have to help the folks that are trying to build <em>today</em>.</p>
<p>I'm happy with the current state of Amplify Gen 2, but any DX will need constant refinement. Myself and the team will continue to make improvements -- but we can only do it with your feedback.</p>
<p>My hope is that I gave you a glimpse into what it took to get t o this point, and inspired you to build with <a target="_blank" href="https://docs.amplify.aws/gen2/">Amplify Gen 2</a> on your next project!</p>
<p>As always, let me know if you have any questions by hitting me up on <a target="_blank" href="https://twitter.com/focusotter">X</a> or <a target="_blank" href="https://www.linkedin.com/in/focusotter/">LinkedIn</a>.</p>
<p>Until next time,</p>
<p>Happy Coding 🦦</p>
]]></content:encoded></item><item><title><![CDATA[How AWS AppSync and Amazon EventBridge unlock real-time data across domains]]></title><description><![CDATA[When AWS announced for AppSync as an EventBrige target, I thought of the use cases this unlocks. For those that aren't aware, it was already possible for AppSync to put an event directly on an event bus. So I figured there was some lambda-less soluti...]]></description><link>https://blog.focusotter.com/how-aws-appsync-and-amazon-eventbridge-unlock-real-time-data-across-domains</link><guid isPermaLink="true">https://blog.focusotter.com/how-aws-appsync-and-amazon-eventbridge-unlock-real-time-data-across-domains</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[focusotter]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[realtime]]></category><category><![CDATA[websockets]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Tue, 06 Feb 2024 06:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706661466500/76ca7826-d547-4ce6-b719-806ae7ce7c5d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2024/01/amazon-eventbridge-appsync-target-buses/">AWS announced for AppSync as an EventBrige target</a>, I thought of the use cases this unlocks. For those that aren't aware, it was already possible for <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2023/04/aws-appsync-publishing-events-amazon-eventbridge/">AppSync to put an event directly on an event bus</a>. So I figured there was some lambda-less solution that would no be unlocked!</p>
<blockquote>
<p>🗒️ I'm not anti-Lambda, I'm pro the-right-tool-for-the-job 😉 If simply passing data around while transforming it, using a Lambda function seem like the wrong tool</p>
</blockquote>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/s2ew8-D7SYY">https://youtu.be/s2ew8-D7SYY</a></div>
<p> </p>
<blockquote>
<p>👆This post is a mix of the above 2 minute video and the <a target="_blank" href="https://github.com/focusOtter/game-brodcaster/blob/main/README.md">two GitHub repos</a>!</p>
</blockquote>
<h2 id="heading-configuring-appsync-to-pass-data-to-eventbridge">Configuring AppSync to pass data to EventBridge</h2>
<p>In hindsight, I may have downplayed the power of EventBridge and AppSync by using <code>localhost:3000</code> in my demo. In truth, real-time subscriptions can scale beyond 3M events per second.</p>
<p>However the principle is the same: Application 1 is a normal app, doing normal things. Then one day Application 2 decides they want to display that data in real-time. No polling, no sharing of resources, just taking an event and passing along the data associated with it.</p>
<p>It's possible for Application 1 to simple invoke an API endpoint provided by Application 2. But they are coupled. Any changes on either side and there needs to be communication. In an event-driven world, this becomes a non-issue. Application 1 would simply put a message on an event bus. It doesn't know or care what downstream services pick it up.</p>
<p>This scenario is exactly what the first repository does.</p>
<p>As mentioned earlier, AWS AppSync has direct support for Amazon EventBridge as a datasource. That means all the <a target="_blank" href="https://blog.focusotter.com/how-to-invoke-appsync-from-a-lambda-function#heading-how-to-allow-lambda-to-sign-with-sigv4">SigV2 signing</a> it handled for you with one line of code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> eventBridgeDS = api.addEventBridgeDataSource(<span class="hljs-string">'gameBusDS'</span>, bus)
</code></pre>
<p>Now this simply forms the connection, however the function that passes the data is also fairly trivial:</p>
<pre><code class="lang-typescript"><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</span>): <span class="hljs-title">PutEventsRequest</span> </span>{
    <span class="hljs-comment">// The data that gets sent to EventBridge</span>
    <span class="hljs-keyword">return</span> {
        operation: <span class="hljs-string">'PutEvents'</span>,
        events: [
            {
                source: ctx.stash.eventBridgeSource,
                detailType: ctx.stash.eventBridgeDetailType,
                detail: { ...ctx.prev.result },
            },
        ],
    }
}

<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</span>) </span>{
    <span class="hljs-comment">//Return the data from EventBridge back to AppSync</span>
    <span class="hljs-keyword">return</span> ctx.prev.result
}
</code></pre>
<h2 id="heading-understanding-eventbridge-rules-and-targets">Understanding EventBridge rules and targets</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/game-receiver">https://github.com/focusOtter/game-receiver</a></div>
<p> </p>
<p>Passing an event to an event bus does effectively nothing. Consumers (targets) subscribe to <em>rules</em> on that event bus. The rule looks at the incoming event payload and says, "based on <em>this</em> matching criteria, I will invoke these targets".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706664243389/33420be3-30b8-4750-9e9a-9fa28c850ad6.png" alt class="image--center mx-auto" /></p>
<p>From the diagram, we can see that an EventBridge bus has a rule setup that will call an AppSync API.</p>
<p>In the second repo, the code that configures this is as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> mybroadcastRule = <span class="hljs-keyword">new</span> events.CfnRule(scope, <span class="hljs-string">'cfnRule'</span>, {
    eventBusName: bus.eventBusName,
    name: <span class="hljs-string">'broadcastToAppSyncRule'</span>,
    eventPattern: {
        source: [<span class="hljs-string">'game.broadcast'</span>],
        [<span class="hljs-string">'detail-type'</span>]: [<span class="hljs-string">'GameUpdated'</span>],
    },
    targets: [
       <span class="hljs-comment">//The targets that care about this message</span>
    ]
})
</code></pre>
<p>Note the <code>eventPattern</code> object. This rule will invoke the <code>targets</code> if the incoming event payload has <code>game.broadcast</code> as the <code>source</code> and <code>GameUpdated</code> as the <code>detail-type</code>.</p>
<p>The next part of this repo is likely where--if you're like me, you'll make the most small-and-hard-to-detect errors:</p>
<p>Adding the target.</p>
<p>This is because we're using the L1 construct, so there aren't any utilities for better mapping EventBridge data to an AppSync operation. You get intellisense, but as someone who is used to working with L2 constructs primarily, feeling something to be desired.</p>
<pre><code class="lang-typescript">targets: [
    {
        id: <span class="hljs-string">'appsyncBroadcastReceiver'</span>,
        arn: props.appsyncEndpointArn,
        roleArn: ebRuleRole.roleArn,
        appSyncParameters: {
            graphQlOperation: props.graphQlOperation,
        },
        inputTransformer: {
            inputPathsMap: {
                createdAt: <span class="hljs-string">'$.detail.createdAt'</span>,
                updatedAt: <span class="hljs-string">'$.detail.updatedAt'</span>,
                name: <span class="hljs-string">'$.detail.name'</span>,
                homeTeamScore: <span class="hljs-string">'$.detail.homeTeamScore'</span>,
                awayTeamScore: <span class="hljs-string">'$.detail.awayTeamScore'</span>,
                currentMessage: <span class="hljs-string">'$.detail.currentMessage'</span>,
                id: <span class="hljs-string">'$.detail.id'</span>,
            },
            inputTemplate: <span class="hljs-built_in">JSON</span>.stringify({
                input: {
                    createdAt: <span class="hljs-string">'&lt;createdAt&gt;'</span>,
                    updatedAt: <span class="hljs-string">'&lt;updatedAt&gt;'</span>,
                    name: <span class="hljs-string">'&lt;name&gt;'</span>,
                    homeTeamScore: <span class="hljs-string">'&lt;homeTeamScore&gt;'</span>,
                    awayTeamScore: <span class="hljs-string">'&lt;awayTeamScore&gt;'</span>,
                    currentMessage: <span class="hljs-string">'&lt;currentMessage&gt;'</span>,
                    id: <span class="hljs-string">'&lt;id&gt;'</span>,
                },
            }),
        },
    },
],
</code></pre>
<p>The first 3 lines should make sense. The 4th line is where we pass in the AppSync operation we're working with.</p>
<blockquote>
<p>🗒️ Running <code>npx @aws-amplify/cli codegen add</code> in a directory that contains the <code>schema.graphql</code> file will generate the needed types for you.</p>
</blockquote>
<p>However, the focus is on the <code>inputPathsMap</code> and the <code>inputTemplate.</code></p>
<h3 id="heading-understanding-the-eventbridge-inputpathsmap">Understanding the EventBridge InputPathsMap</h3>
<p>As someone who doesn't often work with EventBridge, I'll do my best here.</p>
<p>Essentially, a EventBridge payload is an object of whatever depth. I imagine some are deeply nested. Furthermore, EventBridge doesn't care about what data a target needs. As such, the <code>inputPathsMap</code> is your chance to flatten the data and pull out just the values you need.</p>
<p>Similar to how a Lambda function has <code>event</code> or AppSync has <code>context</code>, EventBridge rules put the data under <code>"$"</code>. So the event source is <code>"$.source"</code> while all the arguments that we passed in are under <code>"$.detail"</code>.</p>
<h3 id="heading-understanding-the-eventbridge-inputtemplate">Understanding the EventBridge InputTemplate</h3>
<p>The <code>inputTemplate</code> works in conjunction with the <code>inputPathsMap</code>. If the latter lets us pull out the values we need, then the former allows us to structure it however we like.</p>
<p>Again, an EventBridge rule has no idea how we need our data structured, so it's up to us to tell it.</p>
<blockquote>
<p>🗒️ This isn't <em>entirely</em> true. In my testing, when I put in the wrong structure for my AppSync input and tried to deploy, my deploy would fail while performing some validation on my behalf.</p>
</blockquote>
<p>It's worth noting that the <code>inputTemplate</code> is a string. It's also good to see that the <code>input</code> object is only there because our AppSync schema has an <code>input</code> field for the <code>publishMsgFromEB</code> Mutation.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>These two separate apps form the basis of connecting real-time applications together without them being inherently coupled. Event-driven architectures is a big topic, and I hope this helped you understand just how powerful-yet-approachable it can be when using the AWS CDK to provision your services.</p>
<p>I'm curious to hear your thoughts on this! Let me know in the comments.</p>
<p>Until next time,</p>
<p>Happy Coding 🦦</p>
]]></content:encoded></item><item><title><![CDATA[Hosting a Headless Hashnode UI on AWS Amplify]]></title><description><![CDATA[I've been blogging on Hashnode for several years. I know and have met several members of the team, and for the most part, can say I'm genuinely impressed with it. However, up until recently, I'd also say I'm not your typical Hashnode user. I've alway...]]></description><link>https://blog.focusotter.com/hosting-a-headless-hashnode-ui-on-aws-amplify</link><guid isPermaLink="true">https://blog.focusotter.com/hosting-a-headless-hashnode-ui-on-aws-amplify</guid><category><![CDATA[AWS]]></category><category><![CDATA[AWS Amplify]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Hashnode]]></category><category><![CDATA[headless cms]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Mon, 05 Feb 2024 16:35:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1707145608874/450ff0ff-7691-460b-93c1-24c24944ab72.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been blogging on Hashnode for several years. I know and have met several members of the team, and for the most part, can say I'm genuinely impressed with it. However, up until recently, I'd also say I'm not your typical Hashnode user. I've always wanted the ability to add my own customizations and integrations while still having a dedicated CMS to easily publish articles.</p>
<p>Fortunately, the release of Hashnode's headless UI makes this all possible.</p>
<p>This post will focus on my journey enabling the <a target="_blank" href="https://github.com/Hashnode/starter-kit">Hashnode Starter Kit</a> to run on AWS Amplify, why I decoupled it from its monorepo setup, and what my plans are for the future of my blog.</p>
<h2 id="heading-where-standard-hashnode-fell-short">Where standard Hashnode fell short</h2>
<p>Hashnode has done an excellent job at delivering incremental value. I remember when it was just blogging. Then <a target="_blank" href="https://support.hashnode.com/en/articles/5755362-how-to-map-a-custom-domain">custom domains</a> launched, followed by <a target="_blank" href="https://hashnode.com/post/style-your-hashnode-blog-with-custom-css-ckfwpesyg00ut0es10jgk5uwl">custom CSS</a>. Each iteration felt like it was from the voice of their audience while <a target="_blank" href="https://engineering.hashnode.com/hashnodes-overall-architecture">technical details were made public</a> for folks to learn from.</p>
<p>However, my needs were diverging from the the platforms core audience. I didn't want <a target="_blank" href="https://hashnode.com/ai">another AI tool</a> to offer over embellished ways of writing. I wanted to insert sponsor links. I wanted a customized Focus Otter store that integrated with Stripe, and if I'm being honest, I felt the <a target="_blank" href="https://townhall.hashnode.com/publishing-a-newsletter-is-now-as-easy-as-blogging">newsletter experience</a> offered by Hashnode was too limiting for what I'd like to put out to the audience.</p>
<p>At the same time, my friend Allen Helton shared <a target="_blank" href="https://www.readysetcloud.io/blog/allen.helton/how-i-built-a-serverless-automation-to-cross-post-my-blogs/">details around his custom blogging solution</a>, so I migrated my blog to a custom setup using GitHub Pages and mimicked his setup.</p>
<blockquote>
<p>I failed incredibly hard.</p>
</blockquote>
<p><a target="_blank" href="https://www.readysetcloud.io/blog/allen.helton/how-i-built-a-serverless-automation-to-cross-post-my-blogs/">Allen's setup was customized</a> to his needs, goals, and technical ability. It was the culmination of small improvements over time. So I tried to build my own custom setup and it went exactly as you might expect:</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/focusotter/status/1709583746226114719?s=20">https://twitter.com/focusotter/status/1709583746226114719?s=20</a></div>
<p> </p>
<p>I felt stuck. What I wanted was a launch pad. A way to easily get a custom site up and running...but not too custom where I would spend days/weeks trying to make it my own. Well as always, Hashnode listened and as the author Mark Twain said,</p>
<blockquote>
<p>"History doesn't repeat itself, but it often rhymes."</p>
</blockquote>
<p><a target="_blank" href="https://hashnode.com/headless"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707147963721/6967a42a-ec8d-4f91-917a-b1385b1f1561.png" alt class="image--center mx-auto" /></a></p>
<p>Hashnode had once again delivered a feature that felt straight from the voice of their audience.</p>
<h2 id="heading-customizing-the-headless-ui-experience">Customizing the Headless UI Experience</h2>
<p>A headless UI is a term used to describe a feature where backend functionality is given and exposed to the frontend via API's. For example, a React UI component that creates a table is nice, but probably limiting in how you can customize it. What's better, is to <a target="_blank" href="https://tanstack.com/table/latest">give developers all the tools, hooks, and methods to build their own table and they provide the UI themselves</a>.</p>
<p>Hashnode does the same thing by providing 3 starter repos that come integrated with all the utility functions, API calls, etc for building out a Hashnode-like experience.</p>
<p>Using <a target="_blank" href="https://gql.hashnode.com/">their GraphQL API</a>, developers can craft queries that best match the UI experience they're trying to deliver.</p>
<p>However, there were a two problems I had with their setup.</p>
<ol>
<li><p>It was tailored to Vercel</p>
</li>
<li><p>It was setup as a monorepo</p>
</li>
</ol>
<p>The first one made sense in a way because the 3 starter themes offered (<code>personal</code>, <code>hashnode</code>, <code>enterprise</code>) are NextJS apps, but going back to my needs, I like the integration and customization offered by <a target="_blank" href="https://aws.amazon.com/amplify/?gclid=CjwKCAiAq4KuBhA6EiwArMAw1LVvjrCGSjS2-WFXWIuU0bOO75QZdOcIJjJ-B3DW0ZKaP0waSwTcPBoCTOcQAvD_BwE&amp;trk=66d9071f-eec2-471d-9fc0-c374dbda114d&amp;sc_channel=ps&amp;ef_id=CjwKCAiAq4KuBhA6EiwArMAw1LVvjrCGSjS2-WFXWIuU0bOO75QZdOcIJjJ-B3DW0ZKaP0waSwTcPBoCTOcQAvD_BwE:G:s&amp;s_kwcid=AL!4422!3!646025317188!e!!g!!aws%20amplify!19610918335!148058249160">AWS Amplify</a>. It provides full support for NextJS, while allowing me to build out custom solutions using serverless services</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/AllenHeltonDev/status/1601316734278901761?s=20">https://twitter.com/AllenHeltonDev/status/1601316734278901761?s=20</a></div>
<p> </p>
<p>As for the second option, this is more of a personal gripe. The monorepo setup is made with <code>pnpm</code>, and while <a target="_blank" href="https://docs.aws.amazon.com/amplify/latest/userguide/monorepo-configuration.html">AWS Amplify has support for monorepos</a>, I didn't fee like the <a target="_blank" href="https://github.com/Hashnode/starter-kit">Hashnode starter kit</a> <em>should</em> be one, but rather standalone packages.</p>
<p>My reasoning is that if the goal is to deploy only one of the themes, then despite their being shared resources, the starter templates should live on their own with shared packages and styles in NPM packages. This is similar to <a target="_blank" href="https://vercel.com/templates?framework=next.js&amp;utm_source=google&amp;utm_medium=cpc&amp;utm_campaign=18576682555&amp;utm_campaign_id=18576682555&amp;utm_term=nextjs%20example&amp;utm_content=141035138526_665293501587&amp;gad_source=1&amp;gclid=CjwKCAiAq4KuBhA6EiwArMAw1E3OZAa3GJcmmvtV5Eo2-1CydW7CnRQD9ssHOpTjR8u_Bev82ENh_RoC1wIQAvD_BwE">how Vercel allows for prebuilt templates</a> to be used.</p>
<p>Fortunately, deleting the packages for the monorepo and moving things into a single project repo was very straightforward. If wanting to view what that kind of repo looks like, I have a branch setup that you can take a peek at:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/hashnode-enterprise-standalone/tree/standalone-enterprise">https://github.com/focusOtter/hashnode-enterprise-standalone/tree/standalone-enterprise</a></div>
<p> </p>
<blockquote>
<p>🗒️ The great thing about this setup, is that it's just a NextJS app using the pages directory and SSG. This means it can be deployed <em>anywhere</em>.</p>
</blockquote>
<h2 id="heading-setting-up-my-project-on-aws-amplify">Setting up my project on AWS Amplify</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=ucVK6Z55PZY&amp;t=20s">https://www.youtube.com/watch?v=ucVK6Z55PZY&amp;t=20s</a></div>
<p> </p>
<p>Gone are the days of uploading a project to an S3 bucket, setting up a Cloudfront distribution, and creating a hosted zone in Route53 just to get a site up on the web using AWS.</p>
<p>As you'd come to expect, you just tell Amplify which Git provider you're using (GitHub), which repo you'd like to upload, and it does the work for you.</p>
<p>Because I purchases my domain on AWS, setting up a subdomain was as simple as writing <code>blog</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707149614104/d4d558a8-1e94-4927-88d9-542d922f1cfd.png" alt class="image--center mx-auto" /></p>
<p>This is great because I already use Linktree with Amplify to setup a custom domain for <code>focusotter.com</code></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtube.com/shorts/OSLwLTdjHfg?si=gcM5uMtTNl1AivLe">https://youtube.com/shorts/OSLwLTdjHfg?si=gcM5uMtTNl1AivLe</a></div>
<p> </p>
<p>From there, the only thing left to go to Hashnode and tell it to enable headless UI mode.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707149857352/543c5549-8875-47a2-8eef-6bbf283da965.png" alt class="image--center mx-auto" /></p>
<p>This setup allows me to write my blogs using Hashnode's amazing CMS, schedule them, view analytics, and more, while still having complete control over what the user sees and experiences on my site.</p>
<h2 id="heading-where-i-plan-on-taking-my-blog">Where I plan on taking my blog</h2>
<p>At the time of this writing, I still have the base <code>enterprise</code> starter plan. The "Book a demo" link doesn't do anything and the newsletter doesn't go anywhere.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707149972251/d990cce5-f52c-4265-8fe8-b2f29d442115.png" alt class="image--center mx-auto" /></p>
<p>However, it will now be very easy to customize this experience to my personal needs. A few things I'll be adding in the short term:</p>
<ol>
<li><p><strong>Audio Blogs:</strong> Hashnode removed this feature due to non-usage, but I would love to bring this back!</p>
</li>
<li><p><strong>Translations:</strong> I'd love to use the power of AWS to automatically translate my site into a language that best suits the reader.</p>
</li>
<li><p><strong>Store front:</strong> Folks often ask me where they can get Focus Otter apparel and swag. It's coming soon! 🦦</p>
</li>
<li><p><strong>Premium blog posts:</strong> Converting my blog into a SaaS product where users can signup, and select a <em>very</em> small donation plan to get access to premium blog posts is something I've been wanting to add for a while.</p>
</li>
<li><p>Much more!</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This post showed you how I setup my blog with a custom domain on AWS Amplify while still having it backed by Hashnode's headless UI.</p>
<p>But that wasn't really the point.</p>
<p>I want to highlight what's possible when a company genuinely strives to make life simpler for its users while still allowing them to creatively expand on what they've built.</p>
<p>Hashnode does this very well and for that, I look forward to blogging with them for many years to come.</p>
<p>-- Focus Otter 🦦</p>
]]></content:encoded></item><item><title><![CDATA[Event-Driven Architectures: Real-time Data Across Decoupled Applications]]></title><description><![CDATA[I recently wrote about how to invoke an AppSync API from a Lambda function. While useful and a valid approach, I had 2 gripes with that approach:

Manually signing SigV4 requests

The coupling between the application that sent the request, and the ap...]]></description><link>https://blog.focusotter.com/event-driven-architectures-real-time-data-across-decoupled-applications</link><guid isPermaLink="true">https://blog.focusotter.com/event-driven-architectures-real-time-data-across-decoupled-applications</guid><category><![CDATA[AWS EventBridge]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[focusotter]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[Amazon Web Services]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Mon, 29 Jan 2024 21:24:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706563476039/5a920439-8480-4dc5-94fb-c1ce533622bc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently wrote about <a target="_blank" href="https://hashnode.com/post/clrs2qtft000c08l0dkwh1ajs">how to invoke an AppSync API from a Lambda function</a>. While useful and a valid approach, I had 2 gripes with that approach:</p>
<ol>
<li><p>Manually signing SigV4 requests</p>
</li>
<li><p>The coupling between the application that sent the request, and the application that consumed it.</p>
</li>
</ol>
<p>Fortunately, that all changed when <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2024/01/amazon-eventbridge-appsync-target-buses/">EventBridge announced the addition of AWS AppSync as a direct target</a>🎉</p>
<p>In this post, I'll show how to build out this integration from the position of an event that just got sent to an EventBridge bus. We'll use the AWS CDK to construct this, and show how to test the sample.</p>
<blockquote>
<p>🗒️ The code for this blog post can be found here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/eventbridge-target-appsync">https://github.com/focusOtter/eventbridge-target-appsync</a></div>
<p> </p>
</blockquote>
<h2 id="heading-project-overview">Project Overview</h2>
<p>The repo has all the code, and the readme is detailed, so I'll be going over the services in more detail.</p>
<h3 id="heading-amazon-eventbridge">Amazon EventBridge</h3>
<p>When building out API's there's a subtly contract made between the creator and the consumer. That is: Whenever the creator makes a change, the consumer needs to update to meet that change.</p>
<p>A message bus solves this by serving as an intermediary between the two. Put simply, when application events happen on application 1 like "An order is placed", instead of calling the <code>RewardsAPI</code> , and <code>FulfillmentAPI</code>, a message is put on the message bus:</p>
<blockquote>
<p>🗣️ "Hey, order <code>abc123</code> was placed "</p>
</blockquote>
<p>Now, those downstream teams can grab that event and do whatever they want. Teams do this by setting a matching criteria on the event:</p>
<p>"If the message has the word <code>order</code> in it."</p>
<p>On AWS, the bus is called EventBridge, and the matching criteria is an EventBridge Rule. Those downstream services are called <em>targets</em>.</p>
<h3 id="heading-aws-appsync">AWS AppSync</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=XccNLyZutbU&amp;t=12s">https://www.youtube.com/watch?v=XccNLyZutbU&amp;t=12s</a></div>
<p> </p>
<p>Ok, I have recorded, streamed, and written <em>a lot</em> of content around AppSync. So definitely checkout my channel and previous blogs to get up to speed.</p>
<h2 id="heading-application-overview">Application Overview</h2>
<p>The application in the provided repository uses the AWS CDK to create and provision the mentioned services. The CDK allows us to write TypeScript to version our services instead of clicking through the AWS Console.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> appName = <span class="hljs-string">'eventbridge-to-appsync'</span>

<span class="hljs-keyword">const</span> auth = createCognitoAuth(<span class="hljs-built_in">this</span>, { appName })

<span class="hljs-keyword">const</span> api = createAppSyncAPI(<span class="hljs-built_in">this</span>, { appName, userpool: auth.userPool })

<span class="hljs-keyword">const</span> cfnAPI = api.node.defaultChild <span class="hljs-keyword">as</span> CfnGraphQLApi

<span class="hljs-keyword">const</span> eventBridge = createEventBridge(<span class="hljs-built_in">this</span>, {
    busName: <span class="hljs-string">'eb-appsync-bus'</span>,
    appsyncApiArn: api.arn,
    appsyncEndpointArn: cfnAPI.attrGraphQlEndpointArn,
    graphQlOperation: publishMsgFromEB,
})
</code></pre>
<p>When it comes to creating the AppSync API, there are one main point of interest:</p>
<p><strong>Schema Definition</strong></p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Mutation {
    publishMsgFromEB(<span class="hljs-symbol">msg:</span> String!): String! <span class="hljs-meta">@aws_iam</span>
}

<span class="hljs-keyword">type</span> Subscription {
    <span class="hljs-symbol">onPublishMsgFromEb:</span> String
        <span class="hljs-meta">@aws_cognito_user_pools</span>
        <span class="hljs-meta">@aws_subscribe</span>(<span class="hljs-symbol">mutations:</span> [<span class="hljs-string">"publishMsgFromEB"</span>])
}
</code></pre>
<p>This tells AppSync to protect the <code>publishMsgFromEB</code> mutation with IAM permissions, and that consumers can subscribe to that mutation if they are in a Cognito user pool.</p>
<p>However, the focus is really the EventBridge rule. This forms the connection between the EventBridge bus and the target (AppSync):</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> mycfnRule = <span class="hljs-keyword">new</span> events.CfnRule(scope, <span class="hljs-string">'cfnRule'</span>, {
        eventBusName: bus.eventBusName,
        name: <span class="hljs-string">'mycfnRule'</span>,
        eventPattern: {
            source: [<span class="hljs-string">'sample.source'</span>],
        },
        targets: [
            {
                id: <span class="hljs-string">'myAppsyncTarget'</span>,
                arn: props.appsyncEndpointArn,
                roleArn: ebRuleRole.roleArn,
                appSyncParameters: {
                    graphQlOperation: props.graphQlOperation,
                },
                inputTransformer: {
                    inputPathsMap: {
                        msg: <span class="hljs-string">'$.detail.msg'</span>,
                    },
                    inputTemplate: <span class="hljs-built_in">JSON</span>.stringify({
                        msg: <span class="hljs-string">'&lt;msg&gt;'</span>,
                    }),
                },
            },
        ],
    })
</code></pre>
<p>This is the L1 construct for creating a rule. It specifically listens for the <code>sample.source</code> source on the event payload and invokes our AppSync API in response.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>For detailed instructions on how to test this event, refer to the readme in the repository! In the end, this new integration unlocks a new streamlined approach to creating real-time, event-driven applications that are decoupled from one another.</p>
<p>I'll be posting a bunch more on what you this unlocks so stay tuned for more to come, until then,</p>
<p>Happy coding🦦</p>
]]></content:encoded></item><item><title><![CDATA[Fullstack Image Upload and Download with NextJS and AWS CDK]]></title><description><![CDATA[Amazon Simple Storage Solution (Amazon S3) is one of the oldest AWS services. It's also the one that I was most afraid of when first learning AWS. Turns out, it was never S3 that scared me, but AWS's permission service IAM.
In this post, I'll show yo...]]></description><link>https://blog.focusotter.com/fullstack-image-upload-and-download-with-nextjs-and-aws-cdk</link><guid isPermaLink="true">https://blog.focusotter.com/fullstack-image-upload-and-download-with-nextjs-and-aws-cdk</guid><category><![CDATA[focusotter]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Cognito]]></category><category><![CDATA[S3]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[aws-cdk]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Fri, 26 Jan 2024 20:15:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706294332352/ca52b5e7-15f1-4f4e-8867-671198a89889.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Amazon Simple Storage Solution (Amazon S3) is one of the oldest AWS services. It's also the one that I was most afraid of when first learning AWS. Turns out, it was never S3 that scared me, but AWS's permission service IAM.</p>
<p>In this post, I'll show you how to securely combine a NextJS frontend with an AWS CDK backend. By the end of this post, I hope you see that the parts that I found scary, are nothing more than simple access patterns.</p>
<blockquote>
<p>This post now has a YouTube video with it that shows a full project walkthrough!</p>
</blockquote>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/ueeYD6AqUXo">https://youtu.be/ueeYD6AqUXo</a></div>
<p> </p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Like a medium-rare steak and a good cabernet, this post is best paired with a GitHub repo 👇🏽</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/fullstack-image-upload-download">https://github.com/focusOtter/fullstack-image-upload-download</a></div>
<p> </p>
<p>The repo has all the setup code, and deployment instructions. So instead of repeating that information, I'll instead elaborate on the following sections:</p>
<ol>
<li><p>Authentication with Amazon Cognito</p>
</li>
<li><p>Creating an S3 Bucket with IAM permissions</p>
</li>
</ol>
<h2 id="heading-authentication-with-amazon-cognito">Authentication with Amazon Cognito</h2>
<p>Amazon Cognito is a 3-headed service from AWS. It's made up of the following parts:</p>
<ol>
<li><p><strong>Userpools</strong>: As the name suggests, when a user signs up this is where they go</p>
</li>
<li><p><strong>Userpool Client:</strong> Sometimes you need to provide callback or logout redirect details for social authentication. That's what a client is for.</p>
</li>
<li><p><strong>Identity Pool:</strong> If a userpool is where our users go, an identity pool manages what they can do. This distinction is important.</p>
</li>
</ol>
<p>In the <code>_backend/lib/auth/cognito.ts</code> file from the repo, I setup these 3 pieces of Cognito. What I love about this file is that's 100% boilerplate. Sure I can tweak whether or not a user's email address is their username or if they need to provide a phone number, but typically, I just borrow this file from project to project.</p>
<p>\&gt;🗒️ This project doesn't make use of the Userpool Client, again, I just keep it around to make my life easier if I need it.</p>
<p>The identity pool is the star of the show:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> identityPool = <span class="hljs-keyword">new</span> IdentityPool(
  scope,
  <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-identitypool`</span>,
  {
    identityPoolName: <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-identitypool`</span>,
    allowUnauthenticatedIdentities: <span class="hljs-literal">true</span>,
    authenticationProviders: {
      userPools: [
        <span class="hljs-keyword">new</span> UserPoolAuthenticationProvider({
          userPool: userPool,
          userPoolClient: userPoolClient,
        }),
      ],
    },
  }
)
</code></pre>
<p>Primarily because with it, we get access to an <code>authenticatedRole</code> and <code>unAuthenticatedRole</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706295608667/82ae4835-2622-4c1d-b746-e5f2f786f887.png" alt class="image--center mx-auto" /></p>
<p>We can assign permissions to these roles so that when a user comes to our site, they can assume one of these roles.</p>
<p>One the frontend, we make use of Cognito with the following line:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> withAuthenticator(Home, { signUpAttributes: [<span class="hljs-string">'email'</span>] })
</code></pre>
<h2 id="heading-the-two-sides-of-amazon-s3-creation-and-permission">The two sides of Amazon S3: Creation and Permission</h2>
<p>An S3 bucket is a where we can put a bunch of files. Creating an S3 bucket is one of the easiest things to do with the AWS CDK:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> fileStorageBucket = <span class="hljs-keyword">new</span> s3.Bucket(scope, <span class="hljs-string">`updownBucket`</span>)
</code></pre>
<p>This is all that's needed to <em>create</em> it. From here, we now have to manage the permissions for it.</p>
<p>Fortunately, it's also a fairly boilerplate process.</p>
<p>As you can see in the <code>lib/storage/uploadDownloadBucket.ts</code> file, if you want to enable CORS, the bucket then becomes the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> fileStorageBucket = <span class="hljs-keyword">new</span> s3.Bucket(scope, <span class="hljs-string">`updownBucket`</span>, {
    cors: [
        {
            allowedMethods: [
                s3.HttpMethods.POST,
                s3.HttpMethods.PUT,
                s3.HttpMethods.GET,
                s3.HttpMethods.DELETE,
                s3.HttpMethods.HEAD,
            ],
            allowedOrigins: [<span class="hljs-string">'*'</span>],
            allowedHeaders: [<span class="hljs-string">'*'</span>],
            exposedHeaders: [
                <span class="hljs-string">'x-amz-server-side-encryption'</span>,
                <span class="hljs-string">'x-amz-request-id'</span>,
                <span class="hljs-string">'x-amz-id-2'</span>,
                <span class="hljs-string">'ETag'</span>,
            ],
        },
    ],
})
</code></pre>
<blockquote>
<p>🗒️ The <code>exposedHeaders</code> array is good hygiene as it makes it easier to debug S3 specific errors. Copy Pasta 🍝</p>
</blockquote>
<p>From that, we have an S3 bucket is can be called from the frontend. However, this still doesn't answer the question of <em>who</em> has the permission to call it from the frontend.</p>
<hr />
<p>Amazon S3 bucket permissions are governed by policies. These policies are then given to a role that someone or some other service can assume.</p>
<p>Let's breakdown what that means:</p>
<pre><code class="lang-typescript">
<span class="hljs-comment">// Let signed in users Upload on their own objects in a protected directory</span>
<span class="hljs-keyword">const</span> canUpdateAndReadFromOwnProtectedDirectory = <span class="hljs-keyword">new</span> iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: [<span class="hljs-string">'s3:PutObject'</span>, <span class="hljs-string">'s3:GetObject'</span>],
    resources: [
        <span class="hljs-string">`arn:aws:s3:::<span class="hljs-subst">${fileStorageBucket.bucketName}</span>/protected/\${cognito-identity.amazonaws.com:sub}/*`</span>,
    ],
})
</code></pre>
<p>This policy <strong>allows</strong> the ability to <strong>PUT</strong> and object in S3 and <strong>GET</strong> an object from s3. Which S3 bucket? Which part of the S3 bucket? Well, that's where the <code>resource</code> comes in play.</p>
<p>This policy verify specifically allows access to any file (<code>*</code>) in the <code>/protected/{cognito-identityId}</code> of the bucket we created.</p>
<p>Now this is just a policy. Someone/some service has to have it (aka <em>assume</em> it). For that, we tie a role and a policy together:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">new</span> iam.ManagedPolicy(scope, <span class="hljs-string">'SignedInUserManagedPolicy-test'</span>, {
        description:
            <span class="hljs-string">'managed Policy to allow upload access to s3 bucket by signed in users.'</span>,
        statements: [canUpdateAndReadFromOwnProtectedDirectory],
        roles: [props.authenticatedRole],
    })
</code></pre>
<p>On the frontend, we make use of our S3 bucket with the following two components:</p>
<p><strong>Uploading</strong></p>
<pre><code class="lang-typescript">&lt;StorageManager
  acceptedFileTypes={[<span class="hljs-string">'image/*'</span>]}
  accessLevel=<span class="hljs-string">"protected"</span>
  maxFileCount={<span class="hljs-number">1</span>}
  onUploadSuccess={<span class="hljs-function">(<span class="hljs-params">{ key }</span>) =&gt;</span> {
    setFilename(key <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>)
  }}
/&gt;
</code></pre>
<p><strong>Downloading</strong></p>
<pre><code class="lang-typescript">&lt;StorageImage
  alt={fileName}
  imgKey={fileName}
  accessLevel=<span class="hljs-string">"protected"</span>
/&gt;
</code></pre>
<blockquote>
<p>🗒️ For both of those UI components, notice how I'm passing in the <code>protected</code> string. This will not only store the file name in my bucket, but under the <code>protected/{cognito-identityId}</code> path as well!</p>
</blockquote>
<h2 id="heading-conclusion">Conclusion</h2>
<p>It's worth noting that what I did in this project can be done even easier with the Amplify CLI, however by managing my own resources with the CDK, I can have full control over the policy process and even add on additional configurations that may not be offered by the Amplify CLI such as cacheing my images with a CDN:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.focusotter.com/aws-cdk-for-frontend-developers-amazon-s3-and-cloudfront">https://blog.focusotter.com/aws-cdk-for-frontend-developers-amazon-s3-and-cloudfront</a></div>
<p> </p>
<p>Trading convenience for control is a line that takes practice to get right, but as your applications before more complex the right decision becomes more apparent.</p>
<p>Let me know in the comments if this helped you understand AWS permissions, or if there's anything specific you'd like to see!</p>
<p>Happy coding 🦦</p>
]]></content:encoded></item><item><title><![CDATA[How to Invoke AppSync from a Lambda function]]></title><description><![CDATA[The only time I drink Starbucks is when I travel through the airport. Last time, I noticed they now allow you to skip the line buy ordering from a QR Code. After making your online order, the baristas still get your order on a screen.
In another use ...]]></description><link>https://blog.focusotter.com/how-to-invoke-appsync-from-a-lambda-function</link><guid isPermaLink="true">https://blog.focusotter.com/how-to-invoke-appsync-from-a-lambda-function</guid><category><![CDATA[focusotter]]></category><category><![CDATA[AWS]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[Indie Maker]]></category><category><![CDATA[solopreneur ]]></category><category><![CDATA[serverless]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[aws lambda]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Wed, 24 Jan 2024 17:43:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706118096196/4c1b4d9b-2402-4a25-91d9-8420b5f064b9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The only time I drink Starbucks is when I travel through the airport. Last time, I noticed they now allow you to skip the line buy ordering from a QR Code. After making your online order, the baristas still get your order on a screen.</p>
<p>In another use case, I want to create an AI-generated bedtime story for my kids. When it comes to creating the image, audio, and story, these things take time. Instead of polling, I'd like to be notified when it's done.</p>
<p>Both of those scenarios are examples where you'd want to call AppSync from a Lambda function and what the focus of this post is about.</p>
<blockquote>
<p>📹 This post now has a full video guide!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/-qogqNXlDKM">https://youtu.be/-qogqNXlDKM</a></div>
<p> </p>
</blockquote>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/focusOtter/lambda-invoke-appsync">https://github.com/focusOtter/lambda-invoke-appsync</a></div>
<p> </p>
<blockquote>
<p>🗒️ I provided a repo above incase you want to get straight to the good bits!</p>
</blockquote>
<p>If you're familiar with Lambda functions but new to AWS AppSync, no worries, I have the video for you!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=XccNLyZutbU&amp;t=6s">https://www.youtube.com/watch?v=XccNLyZutbU&amp;t=6s</a></div>
<p> </p>
<h2 id="heading-nodejs-lambda-functions-in-the-aws-cdk">NodeJS Lambda Functions in the AWS CDK</h2>
<p>Fortunately, when it comes to the AWS CDK and Lambda functions, we have the ability to create our resources in TypeScript. From the <code>readme</code>, this is how we create the Lambda function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createInvokeAppSyncFunc = <span class="hljs-function">(<span class="hljs-params">
    scope: Construct,
    props: InvokeAppSyncFuncProps
</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> invokeAppSyncFunc = <span class="hljs-keyword">new</span> NodejsFunction(
        scope,
        <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-invokeAppSyncFunc`</span>,
        {
            functionName: <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-invokeAppSyncFunc`</span>,
            runtime: Runtime.NODEJS_18_X,
            handler: <span class="hljs-string">'handler'</span>,
            entry: path.join(__dirname, <span class="hljs-string">`./main.ts`</span>),
        }
    )

    <span class="hljs-keyword">return</span> invokeAppSyncFunc
}
</code></pre>
<p>Note that aside from some props being passed in, the core is essentially giving it a name, a runtime, and pointing it to the file location.</p>
<h2 id="heading-how-to-allow-lambda-to-sign-with-sigv4">How to allow Lambda to Sign with SigV4</h2>
<p><a target="_blank" href="https://github.com/focusOtter/lambda-invoke-appsync/blob/main/lib/functions/invokeAppSyncFunc/appsyncAuthUtil.ts">This file sucks.</a> I wish it were easier <a target="_blank" href="https://blog.focusotter.com/how-aws-appsync-and-amazon-eventbridge-unlock-real-time-data-across-domains">(now it is!)</a>. The good news is that you never have to modify it. There are some NPM packages out there that allow you to install it, but it's simple enough to just paste in a project.</p>
<p>Let's break it down:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { SignatureV4 } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/signature-v4'</span>
<span class="hljs-keyword">import</span> { Sha256 } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-crypto/sha256-js'</span>
<span class="hljs-keyword">import</span> { defaultProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/credential-provider-node'</span>
<span class="hljs-keyword">import</span> { HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/protocol-http'</span>
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">default</span> <span class="hljs-keyword">as</span> fetch, Request } <span class="hljs-keyword">from</span> <span class="hljs-string">'node-fetch'</span>
</code></pre>
<p>Those imports are needed because the <code>@aws-sdk</code> v3 takes a modular approach. So every bit and piece comes from a standalone package.</p>
<p>Next, we have the following:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// deconstruct the url and create a URL object</span>
<span class="hljs-keyword">const</span> endpoint = <span class="hljs-keyword">new</span> URL(params.config.url)

<span class="hljs-comment">// create something that knows how to let Lambda sign AppSync requests</span>
<span class="hljs-keyword">const</span> signer = <span class="hljs-keyword">new</span> SignatureV4({
    credentials: defaultProvider(),
    region: params.config.region,
    service: <span class="hljs-string">'appsync'</span>,
    sha256: Sha256,
})
</code></pre>
<p>Not too bad 😄 This parses the AppSync GraphQL endpoint in an object containing it's various pieces (protocol, pathname, etc).</p>
<p>In addition, we create a signer by passing in details relating to what we're trying to sign. Sigv4 is similar to creating a JWT or hashing a password in my opinion. Not from a cryptography standpoint, but from a "Hey, I'm going to pass you stuff, and you do all the hard work for me" standpoint.</p>
<p>From there, we keep it going by taking that signing mechanism and passing it the request we are trying to sign:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Setup the request that we are wanting to sign  with our URL and signer</span>
<span class="hljs-keyword">const</span> requestToBeSigned = <span class="hljs-keyword">new</span> HttpRequest({
    hostname: endpoint.host,
    port: <span class="hljs-number">443</span>,
    path: endpoint.pathname,
    method: <span class="hljs-string">'POST'</span>,
    headers: {
        <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
        host: endpoint.host,
    },
    body: <span class="hljs-built_in">JSON</span>.stringify(params.operation),
})

<span class="hljs-comment">// Actually sign the request</span>
<span class="hljs-keyword">const</span> signedRequest = <span class="hljs-keyword">await</span> signer.sign(requestToBeSigned)

<span class="hljs-comment">// Create an authenticated request for fetch</span>
<span class="hljs-keyword">const</span> request = <span class="hljs-keyword">new</span> Request(endpoint, signedRequest)
</code></pre>
<p>With all that in place, we now have a request that is signed, sealed and in the <code>try/catch</code> block, delivered!</p>
<h2 id="heading-usage">Usage</h2>
<p>In the <code>main.ts</code> file, we actually use the <code>AppSyncRequestIAM</code> helper method by invoking it with our AppSync operation:</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> AppSyncRequestIAM({
  config: {
    region: process.env.REGION <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
    url: process.env.APPSYNC_API_URL <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
  },
  operation: {
    operationName: <span class="hljs-string">'BroadcastMessage'</span>,
    query: broadcastMessage,
    variables: {
      msg: event.msg,
    } <span class="hljs-keyword">as</span> BroadcastMessageMutationVariables,
  },
})
</code></pre>
<p>Note that this only pertains to signing the request. It will never get this far to begin with if the following isn't enabled:</p>
<ol>
<li><p>The AppSync API doesn't have <code>IAM</code> authorization enabled</p>
</li>
<li><p>The Schema doesn't have the <code>@aws_iam</code> directive listed on the operation</p>
</li>
<li><p>The Lambda was never given persmissions to invoke AppSync (<code>api.grantMutation(invokeAppSyncFunc</code>)</p>
</li>
</ol>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>Calling AppSync from a Lambda function--or any out-of-band service, requires IAM permissions. This <em>can</em> be tricky, especially the first time. But I hope this post showed you just how simple things can be when you understand the moving pieces.</p>
<p>What are some use cases you'd like to see covered? Let me know in the comments or on social media.</p>
<p>Until then, Happy Coding 🦦</p>
]]></content:encoded></item><item><title><![CDATA[Generate Custom Event Tickets in NodeJS]]></title><description><![CDATA[This post has a YouTube companion! You can check it out here or continue reading 🦦

https://youtu.be/vpQi6Li_zuU?si=ipthTWe5fvExg1Q6
 
Have you ever found yourself in a situation and thought, "We're in 2024, and we're still doing it this way?".
My n...]]></description><link>https://blog.focusotter.com/generate-custom-event-tickets-in-nodejs</link><guid isPermaLink="true">https://blog.focusotter.com/generate-custom-event-tickets-in-nodejs</guid><category><![CDATA[Node.js]]></category><category><![CDATA[image processing]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Thu, 18 Jan 2024 01:46:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1705542164579/0000a091-f765-4e53-a7aa-21f0953d1b30.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This post has a YouTube companion! You can check it out here or continue reading 🦦</p>
</blockquote>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/vpQi6Li_zuU?si=ipthTWe5fvExg1Q6">https://youtu.be/vpQi6Li_zuU?si=ipthTWe5fvExg1Q6</a></div>
<p> </p>
<p>Have you ever found yourself in a situation and thought, "We're in 2024, and we're still doing it <em>this</em> way?".</p>
<p>My neighborhood sold paper tickets so that strangers could come in and see the home decorations. I volunteered to have my home as a tour stop because it sounded like a way to meet new people in the area.</p>
<p>What I didn't realize until the day of was that all the tickets were the same. There was no tracking process. They were just paper tickets handed out after paying through Venmo. Furthermore, the tickets weren't collected or redeemed.</p>
<p>So I came up with a solution where someone could purchase a ticket online and have it texted to them. The ticket would contain a unique code, and their name.</p>
<p>This post will focus on the ticket generation aspect!</p>
<blockquote>
<p>If wanting to skip to trying this out on your own, I have an <a target="_blank" href="https://github.com/focusOtter/ticketer-generator">entire repo</a> that you can use. Follow the readme to get started and begin making your own tickets!</p>
</blockquote>
<h2 id="heading-choosing-a-ticket-template">Choosing a ticket template</h2>
<p>Our goal is to turn the following image:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705531803247/d8ad0cc3-8e7d-4f78-9032-8fd0dee9f0b3.png" alt class="image--center mx-auto" /></p>
<p>Into a template that we can make our own:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705531828746/20ebc728-7d28-4386-81d4-44ba9bd40035.png" alt class="image--center mx-auto" /></p>
<p>For that, I went to <a target="_blank" href="https://www.canva.com/">Canva</a> and simply typed <strong>"Holiday ticket".</strong> From there I tweaked it until I was satisfied, making sure to leave space on the right-hand side for a QR-Code/Barcode, and in the center for the name.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>We'll do all this locally for now. As mentioned, in later posts, we'll see how to automate the ticket distribution process and trigger it from a successful payment.</p>
<p>Inside a new project folder, run the following command from your terminal:</p>
<pre><code class="lang-bash">npm i bwip-js sharp text-to-svg
</code></pre>
<ul>
<li><p><a target="_blank" href="https://www.npmjs.com/package/bwip-js"><strong>bwip-js</strong></a>: Used to create SVG, Buffer, Barcodes, and QR Codes in code</p>
</li>
<li><p><a target="_blank" href="https://sharp.pixelplumbing.com/"><strong>sharp</strong></a><strong>:</strong> Super popular image manipulation library, known for its speed and ease-of-use</p>
</li>
<li><p>t<a target="_blank" href="https://www.npmjs.com/package/text-to-svg"><strong>ext-to-svg</strong></a>: A dependency-less way to create turn text strings into an SVG</p>
</li>
</ul>
<p>With these packages installed, create an <code>index.mjs</code> file and paste in the following imports and variables:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> sharp <span class="hljs-keyword">from</span> <span class="hljs-string">'sharp'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'path'</span>
<span class="hljs-keyword">import</span> bwipjs <span class="hljs-keyword">from</span> <span class="hljs-string">'bwip-js'</span>
<span class="hljs-keyword">import</span> { fileURLToPath } <span class="hljs-keyword">from</span> <span class="hljs-string">'url'</span>
<span class="hljs-keyword">import</span> TextToSVG <span class="hljs-keyword">from</span> <span class="hljs-string">'text-to-svg'</span>

<span class="hljs-keyword">const</span> __filename = fileURLToPath(<span class="hljs-keyword">import</span>.meta.url)
<span class="hljs-keyword">const</span> __dirname = path.dirname(__filename)
</code></pre>
<blockquote>
<p>When using <code>.mjs</code> file extensions, the <code>__dirname</code> variable isn't available. To mimic this behavior we create our on using the <code>fileURLToPath</code> method.</p>
</blockquote>
<p>Next, ensure the templated image you created from Canva is in the root of your directory.</p>
<p>Lastly, create a folder where you would like to have the newly created image saved. This will be in your project's directory, but feel free to call it whatever you like. For me, I'm creating an <code>output/event/holiday-walkthrough</code> folder path.</p>
<p>When all done, your file tree should look similar to the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705532614074/8ea0acd0-f79a-4f28-9817-99c8e87e3a9f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-creating-an-svg-from-text">Creating an SVG from text</h2>
<p>A text string, is different from a text image. While we may look at text on an image and think "Oh, it's <em>just</em> text". Making the distinction upfront helps us think in layers.</p>
<p>Fortunately, the <code>text-to-svg</code> package will do all the work for us.</p>
<p>Paste in the following code in your <code>index.mjs</code> file:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Create an SVG from text</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateSVG</span>(<span class="hljs-params">text</span>) </span>{
  <span class="hljs-keyword">const</span> textToSVG = TextToSVG.loadSync()
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> svg = textToSVG.getSVG(text, {
            <span class="hljs-attr">fontSize</span>: <span class="hljs-number">110</span>,
            <span class="hljs-attr">anchor</span>: <span class="hljs-string">'top'</span>,
            <span class="hljs-attr">attributes</span>: { <span class="hljs-attr">fill</span>: <span class="hljs-string">'black'</span> },
        })
        <span class="hljs-keyword">return</span> Buffer.from(svg)
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error generating SVG:'</span>, err)
        <span class="hljs-keyword">throw</span> err
    }
}
</code></pre>
<p>This function is broken up into 3 main parts:</p>
<ol>
<li><p>Instantiating <code>textToSVG</code> . The <code>loadSync</code> function optionally takes in a font path, but more on that in a future post 😉</p>
</li>
<li><p>Creating the SVG from text. Nothing too crazy here. We give it a font size, an anchor point, and a color. The <a target="_blank" href="https://www.npmjs.com/package/text-to-svg">docs for this package</a> are really helpful, so feel free to play around with the <code>attributes</code>.</p>
</li>
<li><p>Returning an SVG buffer. Large amounts of text are best held as a <em>buffer</em>. This is just an in-memory way to contain large amounts of data. Similar to a variable, but for complex data-types like images.</p>
</li>
</ol>
<h2 id="heading-creating-a-qr-code-and-barcode-in-nodejs">Creating a QR Code and Barcode in NodeJS</h2>
<p>The bwipjs library is fantastic. Really easy to work with and well documented!</p>
<p>Using this library, and a similar approach to creating an SVG of text, I was able to create both a barcode and QR Code without much issue.</p>
<p>To do the same, paste in the following bits of code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Create a QRCode</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateQRCode</span>(<span class="hljs-params">text</span>) </span>{
    <span class="hljs-keyword">let</span> qrcodeBuffer = <span class="hljs-keyword">await</span> bwipjs.toBuffer({
        <span class="hljs-attr">bcid</span>: <span class="hljs-string">'qrcode'</span>,
        text,
        <span class="hljs-attr">scale</span>: <span class="hljs-number">5</span>,
    })

    <span class="hljs-keyword">return</span> qrcodeBuffer
}

<span class="hljs-comment">// Create a Barcode</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateBarcode</span>(<span class="hljs-params">text</span>) </span>{
    <span class="hljs-keyword">let</span> svg = bwipjs.toSVG({
        <span class="hljs-attr">bcid</span>: <span class="hljs-string">'code128'</span>, <span class="hljs-comment">// Barcode type</span>
        text, <span class="hljs-comment">// Text to encode</span>
        <span class="hljs-attr">width</span>: <span class="hljs-number">80</span>,
        <span class="hljs-comment">// includetext: true, // Show human-readable text</span>
        <span class="hljs-attr">textxalign</span>: <span class="hljs-string">'center'</span>, <span class="hljs-comment">// Always good to set this</span>
        <span class="hljs-attr">textcolor</span>: <span class="hljs-string">'ff0000'</span>, <span class="hljs-comment">// Red text</span>
        <span class="hljs-attr">rotate</span>: <span class="hljs-string">'L'</span>,
    })
    <span class="hljs-keyword">return</span> Buffer.from(svg)
}
</code></pre>
<p>Aside from a few attribute differences, the key points are there: Create a buffer and setup some attributes.</p>
<p>I scaled up the QRCode to <code>5</code> because it was a little small by default, but again feel free to play around with that.</p>
<p>For the barcode, I set the <code>bcid</code> to <code>code128</code> based on the docs, but I don't know enough about barcodes to know if this is optimal or not🤷🏽‍♂️</p>
<h2 id="heading-generating-an-image-using-the-sharp-library">Generating an image using the Sharp library</h2>
<p>We have functions to create image buffers, but we need to but that data onto our image template. This is where the <code>sharp</code> library comes in.</p>
<blockquote>
<p>It's worth noting that the <code>sharp</code> library can do <strong>a lot!</strong> We're only using a small portion of it, but essentially any manipulation of images can be done with this package.</p>
</blockquote>
<p>Let's get started by creating a function to create the ticket for us. Paste in the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createTicket</span>(<span class="hljs-params">customerName, ticketId, outputPath</span>) </span>{
    <span class="hljs-keyword">const</span> ticketTemplatePath = path.join(
        __dirname,
        <span class="hljs-string">'./xmas-sample-ticket-hi-res.png'</span>
    )

    <span class="hljs-keyword">const</span> ticket = sharp(ticketTemplatePath)
}
</code></pre>
<p>The function above will take in the params needed to generate a dynamic ticket and is aware of wher to save it. Inside the function, we create the full path of the ticket template and pass it to <code>sharp</code>. The <code>ticket</code> variable is now a <code>sharp</code> instance. Any operations or manipulations we perform on the <code>ticket</code> variable will correspond to the ticket template itself.</p>
<p>To demonstrate that, let's create a <code>try/catch</code> block that will call our <code>generate</code> functions. Paste in the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Generate  barcode for the ticket as buffer</span>
    <span class="hljs-keyword">const</span> barcodeImageBuffer = <span class="hljs-keyword">await</span> generateBarcode(ticketId)

    <span class="hljs-comment">// Generate customer name for ticket as buffer</span>
    <span class="hljs-keyword">const</span> customerNameImageBuffer = <span class="hljs-keyword">await</span> generateSVG(customerName)

    <span class="hljs-keyword">const</span> qrcodeImageBuffer = <span class="hljs-keyword">await</span> generateQRCode(ticketId)

}<span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error creating ticket:'</span>, err)
    <span class="hljs-keyword">throw</span> err
}
</code></pre>
<p>This simple block will create buffers of our SVG. We'll pass these to our <code>ticket</code> variable (aka <code>sharp</code>). At the moment however, it doesn't know <em>where</em> to put them. To fix that, we'll create overlay objects that will have both the buffer, and the positions where each buffer should be.</p>
<p>To better understand that, paste in the following code inside the try-block:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Params to overlay QR code onto the template</span>
<span class="hljs-keyword">const</span> qrCodeOverlay = {
  <span class="hljs-attr">input</span>: qrcodeImageBuffer,
  <span class="hljs-attr">top</span>: <span class="hljs-number">494</span>, <span class="hljs-comment">// X position for QR code</span>
  <span class="hljs-attr">left</span>: <span class="hljs-number">3308</span>,<span class="hljs-comment">// Y position for QR code</span>
}

<span class="hljs-keyword">const</span> barcodeOverlay = {
  <span class="hljs-attr">input</span>: barcodeImageBuffer,
  <span class="hljs-attr">left</span>: <span class="hljs-number">3393</span>,
  <span class="hljs-attr">top</span>: <span class="hljs-number">351</span>, 
}

<span class="hljs-comment">// Params to overlay SVG onto the template</span>
<span class="hljs-keyword">const</span> svgOverlay = {
  <span class="hljs-attr">input</span>: customerNameImageBuffer,
  <span class="hljs-attr">top</span>: <span class="hljs-number">791</span>,
  <span class="hljs-attr">left</span>: <span class="hljs-number">508</span>,
}
</code></pre>
<p>These overlay objects are exactly what <code>sharp</code> is expecting when composing images together.</p>
<blockquote>
<p>🧐 "But Focus Otter, where did you get the values for the <code>top</code> and <code>left</code> keys?</p>
<p>Great question! I'm sure there's a smarter way, but for the sake of ease, I used <a target="_blank" href="https://youtu.be/vpQi6Li_zuU?si=Aja5bsLOi5hhz0Xl&amp;t=166">Figma and it's ruler capabilities!</a></p>
</blockquote>
<p>To show how this all comes together, the last part of this function is to use the <code>composite</code> function from <code>sharp</code> to take our images and turn them into one.</p>
<p>Paste in the following code underneath the overlay objects:</p>
<pre><code class="lang-javascript">
<span class="hljs-keyword">await</span> ticket
  .composite([
    <span class="hljs-comment">// barcodeOverlay,</span>
    qrCodeOverlay,
    svgOverlay,
  ])
  .toFile(outputPath)

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Ticket created!'</span>)
</code></pre>
<blockquote>
<p>I commented out the <code>barcodeOverlay</code> since you probably don't want both a QR Code and a barcode, but feel free to uncomment!</p>
</blockquote>
<p>That's it🎉 You now have a working solution</p>
<h2 id="heading-testing-the-solution">Testing the solution</h2>
<p>To make sure this all works as expected, setup some test data and call the function. For me, I have the following:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Example usage</span>
<span class="hljs-keyword">const</span> customerName = <span class="hljs-string">'Focus Otter'</span>
<span class="hljs-keyword">const</span> hyphenatedCustomerName = customerName.toLowerCase().replace(<span class="hljs-string">' '</span>, <span class="hljs-string">'-'</span>)
createTicket(
    customerName,
    <span class="hljs-string">'some-random-ticket-id'</span>,
    <span class="hljs-string">`output/event/holiday-walkthrough/<span class="hljs-subst">${hyphenatedCustomerName}</span>-ticket.png`</span>
)
</code></pre>
<p>From there, in my terminal, I can run <code>node index.mjs</code> to create my ticket!</p>
<p>What I love about this project is how it can be taken into so many different directions. From simple automations with a CSV, to a full Micro-SaaS with Stripe and AWS (😉)</p>
<p>If you found this helpful, I'd love for you to let me know in the comments!</p>
<p>Until next time, Happy coding 🦦</p>
]]></content:encoded></item><item><title><![CDATA[How I leverage Apple Keynote and WebSockets to maximize engagement during presentations]]></title><description><![CDATA[If there's one thing I'm developing a bit of a reputation for, it's how I put focus on interactivity during my presentations. I don't like doing slides so whenever I do, I try to come up with ideas where the audience can also engage with me during my...]]></description><link>https://blog.focusotter.com/how-i-leverage-apple-keynote-and-websockets-to-maximize-engagement-during-presentations</link><guid isPermaLink="true">https://blog.focusotter.com/how-i-leverage-apple-keynote-and-websockets-to-maximize-engagement-during-presentations</guid><category><![CDATA[React]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[websockets]]></category><category><![CDATA[Developer Advocate]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Fri, 28 Apr 2023 15:21:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1682695158092/61e00e6d-d95e-4988-8803-8f99bf1813f2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If there's one thing I'm developing a bit of a reputation for, it's how I put focus on interactivity during my presentations. I don't like doing slides so whenever I do, I try to come up with ideas where the audience can also engage with me during my talk.</p>
<p>During peak COVID, I was doing a lot of virtual events. To help, I created a demo where I played a live game of Connect4 with one of the attendees while talking about <a target="_blank" href="https://docs.amplify.aws/">AWS Amplify</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://youtu.be/uJf7C8yKRkM">https://youtu.be/uJf7C8yKRkM</a></div>
<p> </p>
<p>That talk was a lot of fun! However, once things started to open up, I wondered how I can bring that kind of engagement to my onstage presentations.</p>
<p>In this post, I'll show how I used Apple Keynote and AWS AppSync subscriptions to create an engaging presentation that included over 400 attendees at the same time!</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/WorksOnMyLocal/status/1649128926340415488?s=20">https://twitter.com/WorksOnMyLocal/status/1649128926340415488?s=20</a></div>
<p> </p>
<h2 id="heading-keynote-setup">Keynote Setup</h2>
<p>Once upon a time, I would spend <strong>weeks</strong> developing slides in code so that I could get the developer fix I craved. This was more self-fulfilling than creating value for attendees. So I went back to Apple Keynote.</p>
<p>I prefer Keynote because of its modern-looking templates and features. But one day, I spotted another feature that got my gears turning:</p>
<p><strong>export to html</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682351712326/c5405fe4-dbda-4a30-8399-271b8d3bc36e.png" alt class="image--center mx-auto" /></p>
<p>Once exported, you're given a folder that contains an <code>index.html</code> file and an <code>assets</code> folder. The <code>assets</code> folder contains all of your slides--it isn't meant to be modified or easy to understand so for my purposes I left it as is and whenever I modified my slides I just re-exported.</p>
<p>The <code>index.html</code> however is more interesting.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682353289315/966540e8-a6c5-4f41-8265-d35eb9396e81.png" alt class="image--center mx-auto" /></p>
<p>What I love about this file is how simple it is. As for what I've learned from testing different scenarios, here ya go:</p>
<ul>
<li><p>The <code>id</code> in the <code>body</code> tag is needed so the slides know where to mount.</p>
</li>
<li><p>Not <em>everything</em> nested in the <code>body</code> tag is needed, but it's best to just leave it alone.</p>
</li>
<li><p>When viewed in the browser (via VS Code's liveServer feature or otherwise), the <code>click</code> event listener is hijacked and used to advance the slides. This is important to know for development purposes.</p>
</li>
<li><p>The <code>div.slideshowNavigator</code> element is used to show a slide preview. To see them, move your cursor to the left side of the screen when viewing your slides in the browser.</p>
</li>
</ul>
<h2 id="heading-keynote-to-react">Keynote to React</h2>
<p>This part isn't novel but was definitely an "aha" moment.</p>
<p>I had the idea of overlaying emojis on my slides but wanted to take advantage of the React ecosystem to do so.</p>
<p>I'm sure there are ways to do this in <strong>[insert some modern framework]</strong>, but since the easiest way to do this was to merge the Keynote HTML file with the HTML file of a React framework, I used Create React App.</p>
<pre><code class="lang-bash">npx create-react-app my-presentation &amp;&amp; <span class="hljs-built_in">cd</span> <span class="hljs-variable">$_</span> &amp;&amp; code .
</code></pre>
<p>From there, I merged the <code>index.html</code> file in create-react-app with the one generated from Keynote:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!--public/index.html--&gt;</span>
<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"%PUBLIC_URL%/favicon.ico"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"theme-color"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"#000000"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span>
            <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</span>
            <span class="hljs-attr">content</span>=<span class="hljs-string">"Web site created using create-react-app"</span>
        /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"apple-touch-icon"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"%PUBLIC_URL%/logo192.png"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"manifest"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"%PUBLIC_URL%/manifest.json"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>React App<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">body</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"body"</span> <span class="hljs-attr">bgcolor</span>=<span class="hljs-string">"black"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">noscript</span>&gt;</span>You need to enable JavaScript to run this app.<span class="hljs-tag">&lt;/<span class="hljs-name">noscript</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"root"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"stageArea"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"z-index: -100"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"stage"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"stage"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"hyperlinkPlane"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"stage"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"slideshowNavigator"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"slideNumberControl"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"slideNumberDisplay"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"helpPlacard"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"waitingIndicator"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"waitingSpinner"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"assets/player/main.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>In addition, I dragged the <code>assets</code> folder given from the Keynote output to the <code>public</code> directory in the React project so the <code>script</code> tag in the HTML file correctly points to it.</p>
<p>Congrats! You now have your slides as a React app! 🎉</p>
<h2 id="heading-testing-interactivity-with-framer-motion">Testing Interactivity with Framer Motion</h2>
<p>The audience attendees will have a web app to add emojis, however for testing, it'll be nice to add a button. Whenever the button is hovered (because the click event is hijacked), we want an emoji to float up on the screen.</p>
<p>Let's first add <a target="_blank" href="https://www.framer.com/motion/">Framer Motion</a> by running the following command in your terminal:</p>
<pre><code class="lang-bash">npm i framer-motion
</code></pre>
<p>From there, add the following component to the App.js file alongside the one that is already there:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> EmojiThrower = <span class="hljs-function">(<span class="hljs-params">{ emoji = <span class="hljs-string">'💯'</span> }</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> randomX = <span class="hljs-built_in">Math</span>.random() * (<span class="hljs-built_in">window</span>.innerWidth - <span class="hljs-number">100</span>)
    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">motion.div</span>
            <span class="hljs-attr">z-index</span>=<span class="hljs-string">{100}</span>
            <span class="hljs-attr">initial</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">y:</span> '<span class="hljs-attr">100vh</span>', <span class="hljs-attr">x:</span> <span class="hljs-attr">randomX</span>, <span class="hljs-attr">opacity:</span> <span class="hljs-attr">1</span>, <span class="hljs-attr">position:</span> '<span class="hljs-attr">absolute</span>' }}
            <span class="hljs-attr">animate</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">opacity:</span> <span class="hljs-attr">0</span>, <span class="hljs-attr">y:</span> <span class="hljs-attr">0</span> }}
            <span class="hljs-attr">exit</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">opacity:</span> <span class="hljs-attr">0</span> }}
            <span class="hljs-attr">transition</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">duration:</span> <span class="hljs-attr">2</span> }}
        &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">fontSize:</span> '<span class="hljs-attr">40px</span>' }}&gt;</span>{emoji}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">motion.div</span>&gt;</span></span>
    )
}
</code></pre>
<p>This is a component that displays an emoji (by default a 💯) at the bottom of the page (<code>100vh</code>) and a random place along the x-axis. Initially, the emoji is fully visible, but over two seconds it both decreases its opacity to zero and brings itself to the top of the screen (<code>0px</code>).</p>
<p>Next, update the <code>App</code> component so that it renders an <code>EmojiThrower</code> every time the button is moused over.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { motion } <span class="hljs-keyword">from</span> <span class="hljs-string">'framer-motion'</span>
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-comment">// ...EmojiThrower code</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [emote, setEmote] = useState([])
    <span class="hljs-keyword">const</span> handleClick = <span class="hljs-function">() =&gt;</span> { setEmote([...emote, <span class="hljs-number">1</span>])}

    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"App"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{</span>
                { <span class="hljs-attr">display:</span> '<span class="hljs-attr">flex</span>', <span class="hljs-attr">justifyContent:</span> '<span class="hljs-attr">flex-end</span>' }
            }&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onMouseOver</span>=<span class="hljs-string">{handleClick}</span>&gt;</span>
                    Show Component
                <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                {emote.map((item, i) =&gt; {
                    return <span class="hljs-tag">&lt;<span class="hljs-name">EmojiThrower</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{i}</span> /&gt;</span>
                })}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App
</code></pre>
<p>Because the <code>slideshowNavigator</code> has a top z-index, the button we'll use for testing has to be moved to <code>flex-end</code> so that the <code>onMouseOver</code> event can fire.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682391145291/de86b4cc-3571-4226-8fb0-a5aa4d102fac.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-creating-and-hosting-an-app-for-your-audience">Creating and hosting an app for your audience</h2>
<p>Being able to click a button and show the emojis locally is nice for testing purposes, for live engagement, we'll need our attendees to click buttons on a deployed app instead.</p>
<p>No need to recreate the wheel here since I already have a repo that does just this 🙂</p>
<p>Fork (and star😉) the following repo and install its dependencies:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/mtliendo/emoji-thrower">https://github.com/mtliendo/emoji-thrower</a></div>
<p> </p>
<p>When running the project with <code>npm run start</code>, you should see a simple app:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682625854186/658779f0-29e3-441b-b247-47532dda5453.png" alt class="image--center mx-auto" /></p>
<hr />
<p>In the code, the section to display emojis is handled by refs:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> emojiRef = useRef([
        { <span class="hljs-attr">emoji</span>: <span class="hljs-string">'🔥'</span>, <span class="hljs-attr">displayText</span>: <span class="hljs-string">'🔥 Fiya'</span> },
        { <span class="hljs-attr">emoji</span>: <span class="hljs-string">'👍🏽'</span>, <span class="hljs-attr">displayText</span>: <span class="hljs-string">'👍🏽 This is great'</span> },
        { <span class="hljs-attr">emoji</span>: <span class="hljs-string">'🦦'</span>, <span class="hljs-attr">displayText</span>: <span class="hljs-string">'🦦 Focus Ottered in!'</span> },
        { <span class="hljs-attr">emoji</span>: <span class="hljs-string">'☁️'</span>, <span class="hljs-attr">displayText</span>: <span class="hljs-string">'☁️ Serverless Cloud'</span> },
        { <span class="hljs-attr">emoji</span>: <span class="hljs-string">'👀'</span>, <span class="hljs-attr">displayText</span>: <span class="hljs-string">'👀 I see you!'</span> },
        { <span class="hljs-attr">emoji</span>: <span class="hljs-string">'🌮'</span>, <span class="hljs-attr">displayText</span>: <span class="hljs-string">'🌮 Gimme moar!!'</span> },
    ])
</code></pre>
<p>Feel free to change those to emojis that best suit your needs.</p>
<p>The next part of the code is the <code>handleClick</code> handler that fires whenever a user clicks a button:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> handleClick = <span class="hljs-keyword">async</span> (emote) =&gt; {
        <span class="hljs-keyword">const</span> reaction = {
            <span class="hljs-attr">icon</span>: emote,
        }
        <span class="hljs-keyword">const</span> channel = <span class="hljs-string">'miami'</span>
        <span class="hljs-keyword">await</span> gen.publish(channel, <span class="hljs-built_in">JSON</span>.stringify(reaction))
    }
</code></pre>
<p>We'll publish data to a WebSocket <em>channel.</em> In my case, I was attending the amazing <a target="_blank" href="https://www.reactmiami.com/">React Miami conference</a>, so I named my channel <code>miami</code>. This doesn't actually matter much to us, but I like to update it in case I'm showing off the code.</p>
<p>The last part to discuss is the <code>await gen.publish(channel, JSON.stringify(reaction))</code> line.</p>
<p>When we create our WebSocket API in AWS, it will provide a file called <code>generated</code> file for us. The forked repo came with this file. Essentially, it exports a function called <code>publish</code> that takes it a channel name, and a string of data. When invoked, it will publish the data to any clients subscribed to that channel.</p>
<p>With the main files in understood, feel free to host this simple web app wherever you like. I prefer to use <a target="_blank" href="https://aws.amazon.com/amplify/">AWS Amplify</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://www.youtube.com/watch?v=ucVK6Z55PZY&amp;t=12s">https://www.youtube.com/watch?v=ucVK6Z55PZY&amp;t=12s</a></div>
<p> </p>
<h2 id="heading-bringing-the-idea-to-life-with-aws-appsync">Bringing the idea to life with AWS AppSync</h2>
<p>This is usually the part where I provide a blurb about how AWS AppSync is a managed GraphQL service by AWS and talk about its benefits. But for this project, we're only interested in leveraging its WebSocket feature so we won't be diving it that.</p>
<p>If you <em>are</em> wanting to learn about that, however, I have a full tutorial for you to checkout:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=OK2B8cp1EyE&amp;t=3s">https://www.youtube.com/watch?v=OK2B8cp1EyE&amp;t=3s</a></div>
<p> </p>
<p>Adding WebSockets to your application is really easy and one of the few times I recommend using the AWS Console instead of <a target="_blank" href="https://blog.focusotter.cloud/aws-cdk-for-frontend-developers-the-ultimate-guide-to-get-you-started">infrastructure as code</a>.</p>
<p>On the <a target="_blank" href="https://aws.amazon.com/appsync/">AWS AppSync service page</a>, select <strong>Get started with AWS AppSync.</strong></p>
<p>This will prompt you to log into your AWS Account. Go ahead and sign in.</p>
<blockquote>
<p>🗒️ If you logged in with your root account, or are new to AWS, be sure to check out my video to make sure your AWS account has basic security measures in place!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=UnqxiSJEZAk&amp;t=10s">https://www.youtube.com/watch?v=UnqxiSJEZAk&amp;t=10s</a></div>
<p> </p>
</blockquote>
<p>Once signed in, select the orange <strong>Create an API</strong> button. At this point, you should see a few options on how to create an API. Select <strong>Create a generic real-time API</strong> and click <strong>start</strong> as shown in the following screenshot.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682636057964/5477d390-ab7d-4e2e-ba9c-3016c0bcf546.png" alt class="image--center mx-auto" /></p>
<p>Give your API a name (any name is fine). After selecting next, your new WebSocket API will be provisioned 🎉</p>
<p>Once done, the <code>src/generated.js</code> file in your presentation project should make more sense: AppSync generates the majority of the code. All we have to do is update the URL and API Key so that it matches our own API credentials.</p>
<p>In the settings tab on the left, copy your API URL and the API key and paste them in the <code>generated.js</code> file in both the emoji-thrower application, and the react-keynote application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682637261053/fc56c6e2-8ed8-4654-9981-d0e7defed69a.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682637294866/5e9c2d94-66ac-4866-8fba-6cf88ea815b1.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { API, graphqlOperation } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-amplify'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
    aws_appsync_graphqlEndpoint:
        <span class="hljs-string">'https://long-string.appsync-api.us-east-1.amazonaws.com/graphql'</span>,
    aws_appsync_region: <span class="hljs-string">'us-east-1'</span>,
    aws_appsync_authenticationType: <span class="hljs-string">'API_KEY'</span>,
    aws_appsync_apiKey: <span class="hljs-string">'da2-bunch-of-alphanumerics'</span>,
}
</code></pre>
<p>With both projects having the correct AWS configuration, all that's left is to update our react-keynote project so that instead of manually firing the emoji on button hover, it listens for events from the EmojiThrower application.</p>
<p>Update the <code>App</code> component in the react-keynote app with the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { subscribe } <span class="hljs-keyword">from</span> <span class="hljs-string">'./generated'</span>
<span class="hljs-keyword">import</span> { motion } <span class="hljs-keyword">from</span> <span class="hljs-string">'framer-motion'</span>
<span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-comment">// ...EmojiThrower component</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [emote, setEmote] = useState([])

<span class="hljs-comment">// this is new</span>
    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> sub = subscribe(<span class="hljs-string">'miami'</span>, <span class="hljs-function">(<span class="hljs-params">{ data }</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(data)
            <span class="hljs-keyword">const</span> icon = <span class="hljs-built_in">JSON</span>.parse(data).icon
            setEmote(<span class="hljs-function">(<span class="hljs-params">prevState</span>) =&gt;</span> [...prevState, icon])
        })

        <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
            sub.unsubscribe()
        }
    }, [])

<span class="hljs-comment">// no more &lt;button/&gt;</span>
    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">"App"</span>&gt;
            {emote.map(<span class="hljs-function">(<span class="hljs-params">item, i</span>) =&gt;</span> {
                <span class="hljs-keyword">return</span> &lt;EmojiThrower key={i} emoji={item} /&gt;
            })}
        &lt;/div&gt;
    )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App
</code></pre>
<p>I did this live for AWS Developer Innovation Day and in the span of 3ish minutes, I handled over 5500 subscription requests!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/live/uNOSzBP40i0?feature=share&amp;t=6588">https://www.youtube.com/live/uNOSzBP40i0?feature=share&amp;t=6588</a></div>
<p> </p>
<p>It helps if you create a QR Code for your audience and add that to one of your slides (don't forget to re-export to HTML and add the assets folder to your project again). Currently, I run the slides in fullscreen on localhost and that seems to work fine for me.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this post we learned how to export an Apple Keynote presentation to HTML. By doing so, we could add interactivity to our presentation so that our audience isn't just looking over our slides. We used emojis in this case, but the possibilities are really dependent on how creative you want to be.</p>
<p>Additionally, we saw how easy it is to add in AWS AppSync's subscriptions. Doing so allows us to only make use of the live interaction parts while not getting fully mixed in with the GraphQL side of things (though many apps will benefit from that 😉)</p>
<p>I hope you enjoyed this as much as I did putting it together!</p>
<p>Until next time, happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[AWS CDK for Frontend Developers: Multi-Stage Deploys with Github Actions]]></title><description><![CDATA[Throughout this series, we've discussed how to build backend infrastructure using the AWS CDK. In doing so, we've touched on several services and concepts. However, to make sure our application is working as intended, we'll need to deploy our environ...]]></description><link>https://blog.focusotter.com/aws-cdk-for-frontend-developers-multi-stage-deploys-with-github-actions</link><guid isPermaLink="true">https://blog.focusotter.com/aws-cdk-for-frontend-developers-multi-stage-deploys-with-github-actions</guid><category><![CDATA[AWS]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Devops]]></category><category><![CDATA[aws-cdk]]></category><category><![CDATA[Startups]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Mon, 27 Mar 2023 22:07:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679954274069/63d745e9-aedc-425b-b55c-854adabfd402.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Throughout this series, we've discussed how to build backend infrastructure using the AWS CDK. In doing so, we've touched on several services and concepts. However, to make sure our application is working as intended, we'll need to deploy our environment to AWS.</p>
<p>Fortunately for us, we laid a solid foundation for deploying our application when we first set it up in an earlier lesson. For the most part, this chapter can stand alone from the other chapters, though we'll use the project we've built so far so that we have something to deploy. However, if simply wanting to know how to deploy a CDK application, this guide is still for you.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.focusotter.cloud/aws-cdk-for-frontend-developers-the-ultimate-guide-to-get-you-started">https://blog.focusotter.cloud/aws-cdk-for-frontend-developers-the-ultimate-guide-to-get-you-started</a></div>
<p> </p>
<p>In that previous post, we used a combination of the<code>git-branch</code> library and the <code>app.node.tryGetContext()</code> method to dynamically create a stack that was dependent on the environment we were in.</p>
<p>Due to our diligence in the beginning, in this chapter, we'll see how easy it is to not only deploy our backend to AWS with GitHub actions but what a basic setup will look like when wanting to have a separate deployment flow for our <code>develop</code> and <code>main</code> branches.</p>
<h2 id="heading-automation-made-simple-with-github-actions">Automation made simple with GitHub Actions</h2>
<p><a target="_blank" href="https://github.com/features/actions">GitHub Actions</a> is a powerful tool that allows you to automate workflows. With GitHub Actions, you can create custom workflows that automatically build, test and deploy your code whenever you push changes to your repository.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679954110936/ea75cf04-c368-4d9a-8a1f-084f8792ef9b.png" alt class="image--center mx-auto" /></p>
<p>A GitHub action is defined in YML and is picked up by GitHub by being placed in a <code>.github/workflows</code> folder.</p>
<p>As seen in the image above, we'll take advantage of this by specifying a workflow with two different flows. In particular, depending on whether the event is a commit to our <code>develop</code> branch or a merge to our <code>main</code> branch, we'll do the following:</p>
<ol>
<li><p>Checkout our application from GitHub</p>
</li>
<li><p>Perform the steps needed to build the application</p>
</li>
<li><p>Deploy the application</p>
</li>
</ol>
<p>In your project, create the following directories and file: <code>.github/workflows/aws.yml</code></p>
<p>In that file, paste the following.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="274a5c8f4bff90e58d3cb9fd351ecc59"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/mtliendo/274a5c8f4bff90e58d3cb9fd351ecc59" class="embed-card">https://gist.github.com/mtliendo/274a5c8f4bff90e58d3cb9fd351ecc59</a></div><p> </p>
<blockquote>
<p>🗒️ Because our deployment is dependent on the branch we are on, now is a good time to make sure we are on the <code>develop</code> branch and not the <code>main</code> branch. Also take a moment to push your project to GitHub so that the project is available.</p>
</blockquote>
<p>A quick explainer of the lines is as follows:</p>
<ol>
<li><p><strong>lines 1-10</strong>: We give the action a name of our choosing, and define the criteria needed for our action to run</p>
</li>
<li><p><strong>lines 13-15:</strong> Our workflow is called <code>aws_cdk</code>, if the event is a PR <code>merge</code>, or a <code>push</code>, then spin up an ubuntu server to run the following steps.</p>
</li>
<li><p><strong>lines 17-22:</strong> The server is configured with <code>git</code> but that's it. So we create a step to checkout our repo and set up node version 18 using official GitHub-managed actions. Because NodeJS comes with NPM, we run <code>npm install</code></p>
</li>
<li><p><strong>lines 23-31:</strong> We need to install the <code>CDK</code> on our server. While this can be done manually, there is a popular and well-maintained 3rd-party action that does this. With it installed, we run the <code>cdk diff</code> command which is similar in spirit to <code>git diff</code> to view changes between what is local vs what is deployed. To do that, we provide AWS credentials as environment variables and our GitHub token environment variable for pull-request support.</p>
</li>
<li><p><strong>lines 32-41:</strong> We deploy our application using the same GitHub action as the previous step except for this time we call the <code>deploy</code> command. Because we can't accept yes/no CLI prompts in this server, we pass a flag to auto-approve.</p>
</li>
</ol>
<blockquote>
<p>🗒️ If following along through the course, it's a good idea to run <code>npx aws-cdk synth</code> locally before deploying. This will compile your code to CloudFormation which serves as a good sanity check that there are no configuration errors.</p>
</blockquote>
<p>GitHub action permissions are on a per-repo basis. Ensure your repo is configured correctly:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679953100371/39781649-6170-415e-8d17-74fed676d18f.png" alt class="image--center mx-auto" /></p>
<p>Lastly, recall our GitHub action uses environment variables to avoid us having to pass in our raw AWS credentials (which should <strong>never</strong> be done!).</p>
<p>The simplest way to get those is to grab them from your machine. When you set up your AWS account, those were stored in a <code>~/.aws</code> directory. Grab those values and paste them into GitHub as shown in the following screenshot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679953294822/2e2c0389-1e31-48b1-a565-97dcd9221aa2.png" alt class="image--center mx-auto" /></p>
<p>With our application configured, you can now deploy your code and view the action run from the <strong>Actions</strong> tab:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679953451589/d155be6c-aada-47dc-8f41-7ca287a2c253.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-deploy-to-production">Deploy to Production</h2>
<p>On every commit, we have our code deploying our <code>develop</code> environment, however, when a merge occurs on the <code>main</code> branch, we want to deploy our <code>production</code> environment.</p>
<p>To accomplish this, all we have to do is update our <code>cdk.context.json</code> file. Paste in the following:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"globals"</span>: {
        <span class="hljs-attr">"appName"</span>: <span class="hljs-string">"trip-logger"</span>,
        <span class="hljs-attr">"region"</span>: <span class="hljs-string">"us-east-1"</span>,
        <span class="hljs-attr">"appDescription"</span>: <span class="hljs-string">"A stack for a Travel pic viewer"</span>
    },
    <span class="hljs-attr">"environments"</span>: [
        {
            <span class="hljs-attr">"environment"</span>: <span class="hljs-string">"develop"</span>,
            <span class="hljs-attr">"branchName"</span>: <span class="hljs-string">"develop"</span>,
            <span class="hljs-attr">"s3AllowedOrigins"</span>: [<span class="hljs-string">"*"</span>]
        },
        {
            <span class="hljs-attr">"environment"</span>: <span class="hljs-string">"production"</span>,
            <span class="hljs-attr">"branchName"</span>: <span class="hljs-string">"main"</span>,
            <span class="hljs-attr">"s3AllowedOrigins"</span>: [<span class="hljs-string">"*"</span>]
        }
    ]
}
</code></pre>
<p>Commit your changes and wait for your <code>develop</code> branch to deploy. Once done, create a pull request to your <code>main</code> branch and merge it. You should see your <code>production</code> environment deploy 🎉</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this chapter, we saw how to deploy a CDK application to AWS using GitHub actions. While there are entire courses and varying opinions on how to manage your workflows and architecture, this method shows off the configuration options available while keeping the deployment straightforward.</p>
<p>While deploying our backend is useful on its own, in a fullstack application, you'll likely need the values from these services. In the next chapter, we'll see how this can be accomplished as we shift our focus to our frontend application.</p>
<p>Until then, happy coding!</p>
<p>🦦</p>
]]></content:encoded></item><item><title><![CDATA[AWS CDK for Frontend Developers: Building Modern APIs with AWS AppSync]]></title><description><![CDATA[AWS AppSync is a fully-managed GraphQL API service and the last service that we'll add to this backend stack. Due to it being an API, it'll read and write data to DynamoDB, authorize requests with Cognito, and can even perform custom logic with Lambd...]]></description><link>https://blog.focusotter.com/aws-cdk-for-frontend-developers-building-modern-apis-with-aws-appsync</link><guid isPermaLink="true">https://blog.focusotter.com/aws-cdk-for-frontend-developers-building-modern-apis-with-aws-appsync</guid><category><![CDATA[AWS]]></category><category><![CDATA[Startups]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[serverless]]></category><category><![CDATA[aws-cdk]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Wed, 22 Mar 2023 03:05:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679454234444/c7fd94db-007f-46b7-81f3-869e23d85f75.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://aws.amazon.com/appsync/">AWS AppSync</a> is a fully-managed GraphQL API service and the last service that we'll add to this backend stack. Due to it being an API, it'll read and write data to DynamoDB, authorize requests with Cognito, and can even perform custom logic with Lambda.</p>
<p>At this point in the series, we should have a solid grasp on using various L2 constructs, passing arguments to them, and even a bit of knowledge with both setting permissions in policies and adding them to roles.</p>
<p>In this chapter, we'll expose ourselves to AppSync, both in terms of how it relates to GraphQL, but also as a service in the AWS ecosystem. By the end of this chapter, you'll have the tools in place to develop your next API with AppSync while understanding how it can best integrate as part of your backend architecture.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679156038320/313068be-b268-4a85-9167-4e8f4019030e.png?w=1600&amp;h=840&amp;fit=crop&amp;crop=entropy&amp;auto=compress,format&amp;format=webp&amp;auto=compress,format&amp;format=webp" alt="AWS CDK for Frontend Developers: The Ultimate Guide to Get You Started" /></p>
<p>From the perspective of our Trip Logger application, this will complete the backend services in this stack!</p>
<p>AppSync is a powerful service, and if this is your first experience with it, be ready because it's going to change how you think about API development.</p>
<h2 id="heading-aws-appsync">AWS AppSync</h2>
<p>AppSync can be split up into two conceptual parts.</p>
<ol>
<li><p>The GraphQL part: This includes features that are common to anyone building a GraphQL API</p>
</li>
<li><p>The fully-managed on AWS part: These are the parts that make it different from other GraphQL APIs.</p>
</li>
</ol>
<p>Let's start by first talking about what GraphQL is and the benefits it gives to indie creators, and SaaS developers.</p>
<h3 id="heading-graphql-in-a-nutshell">GraphQL in a Nutshell</h3>
<p>While there are complete books, courses and such on GraphQL, I'll outline the 3 main parts to working with GraphQL on the backend:</p>
<h4 id="heading-graphql-schema">GraphQL Schema</h4>
<p>A schema is a contract for your API. It's made up of Queries, Mutations and Subscriptions. So instead of the traditional <code>get</code>,<code>put</code>,<code>update</code> and <code>delete</code> routes in REST, with GraphQL you use a <code>Query</code> to fetch data, a <code>Mutation</code> to update data (including deleting it), and a <code>Subscription</code> to listen for updates.</p>
<p>An example schema would look like the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679415807614/9b110c01-7185-4452-86c6-70ffda650b37.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>🗒️ The general flow for defining an operation on a type is:</p>
<p><code>name(arguments): returnType</code></p>
<p>Also, note that as opposed to TypeScript, values are optional by default in GraphQL. To mark a value as non-null (aka required), add a <code>!</code>.</p>
</blockquote>
<p>In terms of supported scalar types, GraphQL supports <code>String</code>, <code>Int</code>, <code>Boolean</code> , <code>ID</code>, and <code>Float</code>. In case you're wondering, <code>ID</code> is a string, but it's marking it as being unique.</p>
<h4 id="heading-data-sources">Data Sources</h4>
<p>The second concept of GraphQL is a data source. Nothing too fancy about this. A data source is simply where you are going to get your data from. This can be a database, a JSON file, a public API etc.</p>
<h4 id="heading-resolvers">Resolvers</h4>
<p>If a schema defines the data, and data sources are where the data lives, then a <em>resolver</em> sits in the middle and describes how to get that data. However, as opposed to a RESTful API, there aren't multiple routes to manage. GraphQL does this all through a single HTTP endpoint.</p>
<p>This is typically done in a GraphQL server. However, it can also be done in a Lambda function. The point here is that these are functions that are aware of the incoming request and can perform some logic based on the Query or Mutation operation.</p>
<p>As a frontend developer, this is similar in spirit to a reducer in libraries like <a target="_blank" href="https://redux.js.org/introduction/examples#shopping-cart">Redux</a> where there is a central hub that manages the state when actions are dispatched.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679416891939/b44803fd-5738-43e8-afd6-c0ff281a8362.png" alt class="image--center mx-auto" /></p>
<p>This is the appeal of GraphQL. When client applications ask for data, they don't need to know how the data is fetched, or where it comes from. Take the following schema for example.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679417402919/c8cc3bb8-9144-4502-8d12-55c374ff5c78.png" alt class="image--center mx-auto" /></p>
<p>When the frontend calls the <code>getSocialRep</code> query operation, they don't need to know that this is going to call the APIs for GitHub, Instagram, and Twitter. All they have to do is use a single endpoint to describe the data they need, and the resolvers connect to the various data sources to fulfill their request.</p>
<h3 id="heading-graphql-on-aws">GraphQL on AWS</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679419219909/58553736-d9e8-4182-8642-9b6b6682235b.png" alt class="image--center mx-auto" /></p>
<p>The problem with running GraphQL in a server is that you are paying for the server even when no one is using it and servers need to have their capacity scaled up and down manually based on usage.</p>
<p>To solve that, a lot of folks run GraphQL in a Lambda function. This solves both scaling and price. However, Lambda functions have their own limitations when it comes to cold starts, and need to manage memory capacity.</p>
<p>AWS AppSync is a fully-managed, fully-serverless solution for both scenarios. Because it's on AWS, it natively integrates with Cognito and DynamoDB. In addition, it automatically comes with subscription support by generating a WebSocket endpoint.</p>
<p>I've written and recorded a bunch of content on AppSync--even before joining the team, however, if wanting a quick start video on building a fullstack app with it, I created one that uses a great getting-started framework known as <a target="_blank" href="https://docs.amplify.aws/">AWS Amplify</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://www.youtube.com/watch?v=OK2B8cp1EyE">https://www.youtube.com/watch?v=OK2B8cp1EyE</a></div>
<p> </p>
<p>With tens of thousands of customers, and <em>billions</em> of operations every single day, if you've ever had <a target="_blank" href="https://aws.amazon.com/solutions/case-studies/taco-bell/"><strong>Taco Bell delivered to you</strong></a>, <a target="_blank" href="https://aws.amazon.com/blogs/media/watch-now-hbo-max-uses-canaries-on-aws-for-outside-in-validation/"><strong>watched a show on HBO Max</strong></a>, <a target="_blank" href="https://www.youtube.com/watch?v=1qYBEcG-fV4&amp;t=1s"><strong>saw stats on a live Ferrari F1 race</strong></a>, or <a target="_blank" href="https://twitter.com/AWSstartups/status/1040844414627864576"><strong>purchased a ticket from Ticketmaster</strong></a>, then you've already interacted with AppSync.</p>
<h2 id="heading-how-to-create-an-appsync-api-in-the-cdk">How to Create an AppSync API in the CDK</h2>
<p>Now that we talked about the pieces of a GraphQL API, let's put them to use.</p>
<h4 id="heading-creating-an-appsync-schema">Creating an AppSync schema</h4>
<p>The schema serves as our contract so it's common to start here first.</p>
<p>To start, in <code>lib/api/graphql</code> create a file called <code>schema.graphql</code> and paste in the following:</p>
<pre><code class="lang-graphql"><span class="hljs-comment"># lib/api/graphql/schema.graphql</span>
<span class="hljs-keyword">type</span> Query {
    getTrip(<span class="hljs-symbol">id:</span> ID!): Trip <span class="hljs-meta">@aws_cognito_user_pools</span> <span class="hljs-meta">@aws_iam</span>
    <span class="hljs-symbol">listTrips:</span> [Trip]! <span class="hljs-meta">@aws_cognito_user_pools</span> <span class="hljs-meta">@aws_iam</span>
    getUser(<span class="hljs-symbol">id:</span> ID!): User <span class="hljs-meta">@aws_cognito_user_pools</span>
}

<span class="hljs-keyword">type</span> Mutation {
    createTrip(<span class="hljs-symbol">input:</span> TripCreateInput!): Trip <span class="hljs-meta">@aws_cognito_user_pools</span>
    updateTrip(<span class="hljs-symbol">input:</span> TripUpdateInput!): Trip <span class="hljs-meta">@aws_cognito_user_pools</span>
    deleteTrip(<span class="hljs-symbol">id:</span> ID!): Trip <span class="hljs-meta">@aws_cognito_user_pools</span>
}

<span class="hljs-keyword">type</span> Subscription {
    <span class="hljs-symbol">onTripCreate:</span> Trip <span class="hljs-meta">@aws_subscribe</span>(<span class="hljs-symbol">mutations:</span> [<span class="hljs-string">"createTrip"</span>])
}
</code></pre>
<p>As mentioned earlier. We start things off by defining the operations we'd like to have.</p>
<p>However, there are three new <em>things</em> here: <code>@aws_cognito_user_pools</code> , <code>@aws_iam</code> , and <code>@aws_subscribe</code> .</p>
<p>These are known in GraphQL as directives and generally speaking, they are used to add special functionality to an operation. This is similar to higher-ordered functions in React.</p>
<p>In the case of these directives specifically, here is the breakdown of what each directive means in plain speak:</p>
<ul>
<li><p><code>@aws_cognito_user_pools</code>: This operation can only be called if the person calling it is in a Cognito userpool (aka, if they're signed in).</p>
</li>
<li><p><code>aws_iam</code>: This operation can only be called if the <strong>person or AWS service</strong>, has a policy applied to their role that allows them to do so.</p>
</li>
<li><p><code>@aws_subscribe</code>: Listens for a change to the provided mutations and sends back the return type, <code>Trip</code> in this case, via a WebSocket when that happens.</p>
</li>
</ul>
<p>You'll notice on the operations, the return type is often <code>trip</code> (or an array of trips with <code>[trip]</code>) and some operations take in an input. Let's define those as well.</p>
<p>Add the following to our schema:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Trip <span class="hljs-meta">@aws_cognito_user_pools</span> <span class="hljs-meta">@aws_iam</span> {
    <span class="hljs-symbol">id:</span> ID!
    <span class="hljs-symbol">createdAt:</span> AWSDateTime!
    <span class="hljs-symbol">updatedAt:</span> AWSDateTime!
    <span class="hljs-symbol">title:</span> String!
    <span class="hljs-symbol">description:</span> String!
    <span class="hljs-symbol">imgKey:</span> String!
}

<span class="hljs-keyword">type</span> User <span class="hljs-meta">@aws_cognito_user_pools</span> {
    <span class="hljs-symbol">id:</span> ID!
    <span class="hljs-symbol">createdAt:</span> AWSDateTime! <span class="hljs-comment"># 2023-02-16T16:07:14.189Z</span>
    <span class="hljs-symbol">updatedAt:</span> AWSDateTime!
    <span class="hljs-symbol">username:</span> String!
    <span class="hljs-symbol">email:</span> AWSEmail!
}

<span class="hljs-comment"># No "id" is required because we will create one automatically.</span>
<span class="hljs-keyword">input</span> TripCreateInput {
    <span class="hljs-symbol">title:</span> String!
    <span class="hljs-symbol">description:</span> String!
    <span class="hljs-symbol">imgKey:</span> String!
}

<span class="hljs-keyword">input</span> TripUpdateInput {
    <span class="hljs-symbol">id:</span> ID!
    <span class="hljs-symbol">title:</span> String!
    <span class="hljs-symbol">description:</span> String!
    <span class="hljs-symbol">imgKey:</span> String!
}
</code></pre>
<p>We again use directives on our <code>Trip</code> type to effectively say, "You have to be authenticated in a user pool or have a policy applied to receive this return type."</p>
<blockquote>
<p>🗒️ <code>AWSDateTime</code> is a custom scalar offered by AppSync. It's a <code>String</code> type, but AppSync will automatically check to make sure it's a ISO formatted date (<code>2023-02-16T16:07:14.189Z</code>)</p>
<p>The full set of AppSync scalars can be found in <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html">the AppSync docs</a>.</p>
<p>🗒️ Now is a good time to review our <code>postConfirmation</code> function. Notice how the data we store in the database overlap with what we have in for a <code>User.</code></p>
</blockquote>
<h4 id="heading-creating-an-appsync-api">Creating an AppSync API</h4>
<p>Now that we have an idea of what will be needed, let's call our yet-to-be-created AppSync API and give it the needed props.</p>
<p>In the <code>lib/backend-trip-post-stack.ts</code> file, paste in the following <strong>under</strong> our <code>cognitoAuth</code> function:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// import {createAppSyncTripAPI} from './api/appsync.ts'</span>
<span class="hljs-keyword">const</span> tripAPI = createAppSyncTripAPI(<span class="hljs-built_in">this</span>, {
    appName: context.appName,
    env: context.environment,
    unauthenticatedRole: cognitoAuth.identityPool.unauthenticatedRole,
    userpool: cognitoAuth.userPool,
    travelDB,
    userDB,
})
</code></pre>
<p>As seen from the directives in our schema, access to our API can be provided via a user pool, or some policy on their role. To account for that, we are passing in our user pool, and our <strong>unauthenticatedRole</strong> from our Cognito identity pool.</p>
<p>Also, because we know our <code>travelDB</code> table and <code>userDB</code> table are what we will be performing operations against, we provided those as well.</p>
<blockquote>
<p>🗒️ This again shows the power of the CDK.</p>
<p>Through the use of TypeScript, we simply pass constructs around just like in any other TypeScript application.</p>
</blockquote>
<h4 id="heading-initializing-the-l2-appsync-construct">Initializing the L2 AppSync Construct</h4>
<p>To use the AppSync L2 construct, create a file called <code>/lib/api/appsync.ts</code> and paste in the following:</p>
<pre><code class="lang-graphql">import { envNameContext } from './../../cdk.context.d'
import { Construct } from 'constructs'
import * as awsAppsync from 'aws-cdk-lib/aws-appsync'
import * as path from 'path'
import { UserPool } from 'aws-cdk-lib/aws-cognito'
import { Table } from 'aws-cdk-lib/aws-dynamodb'
import { IRole } from 'aws-cdk-lib/aws-iam'

<span class="hljs-keyword">type</span> AppSyncAPIProps = {
    <span class="hljs-symbol">appName:</span> string
    <span class="hljs-symbol">env:</span> envNameContext
    <span class="hljs-symbol">unauthenticatedRole:</span> IRole
    <span class="hljs-symbol">userpool:</span> UserPool
    <span class="hljs-symbol">travelDB:</span> Table
    <span class="hljs-symbol">userDB:</span> Table
}

export function createAppSyncTripAPI(<span class="hljs-symbol">scope:</span> Construct, <span class="hljs-symbol">props:</span> AppSyncAPIProps) {
    const api = new awsAppsync.GraphqlApi(scope, 'TripAPI', {
        <span class="hljs-symbol">name:</span> `<span class="hljs-variable">$</span>{props.appName}-<span class="hljs-variable">$</span>{props.env}-TripAPI`,
        <span class="hljs-symbol">schema:</span> awsAppsync.SchemaFile.fromAsset(
            path.join(__dirname, './graphql/<span class="hljs-keyword">schema</span>.graphql')
        ),
        <span class="hljs-symbol">authorizationConfig:</span> {
            <span class="hljs-symbol">defaultAuthorization:</span> {
                <span class="hljs-symbol">authorizationType:</span> awsAppsync.AuthorizationType.USER_POOL,
                <span class="hljs-symbol">userPoolConfig:</span> {
                    <span class="hljs-symbol">userPool:</span> props.userpool,
                },
            },
            <span class="hljs-symbol">additionalAuthorizationModes:</span> [
                { <span class="hljs-symbol">authorizationType:</span> awsAppsync.AuthorizationType.IAM },
            ],
        },
        <span class="hljs-symbol">logConfig:</span> {
            <span class="hljs-symbol">fieldLogLevel:</span> awsAppsync.FieldLogLevel.ALL,
        },
    })

    api.grantQuery(props.unauthenticatedRole, 'getTrip', 'listTrips')

    return api
}
</code></pre>
<p>Skipping past the imports and the types, we'll look at the main parts of the construct's config object:</p>
<ol>
<li><p><code>schema</code>: Here we load in the <code>schema.graphql</code> we created earlier.</p>
</li>
<li><p><code>authorizationConfig</code>: This section configures the API's authorization settings. It specifies that the default authorization type is <code>USER_POOL</code>, meaning it uses AWS Cognito User Pools for authentication. Additionally, it accepts additional authorization modes. In our case, we're specifying <code>IAM</code> authorization type as an alternative mode, passing in the IAM role from our Cognito identity pool.</p>
</li>
<li><p><code>logConfig</code>: This section enables logging for the API, with the field log level set to <code>ALL</code>. This means all resolvers' logs will be captured, including errors, information, and field-level data. When we start testing our API on the frontend, we'll see how to view these logs.</p>
</li>
</ol>
<p>When we created our API we allowed users/services with the correct IAM permissions access. To update those roles so that they indeed have access to certain operations, we use the helper method <code>grantQuery</code> and pass in the role, and the queries we'd like that role to have access to.</p>
<p>With intellisense, it's possible to see the other operations as well:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679432439959/fd211758-b9c3-4e21-a15f-22792ce9d7d9.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-adding-a-data-source-to-an-appsync-api">Adding a Data Source to an AppSync API</h4>
<p>We passed in our <code>travelDB</code> and <code>userDB</code> because that is where our <code>Trip</code> and <code>User</code> information is persisted. To add those as data sources to our API, add the following just before our <code>return</code> statement:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> TripDataSource = api.addDynamoDbDataSource(
  <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-TripDataSource`</span>,
  props.travelDB
)
<span class="hljs-keyword">const</span> UserDataSource = api.addDynamoDbDataSource(
  <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-UserDataSource`</span>,
  props.userDB
)
</code></pre>
<p>It's worth taking some time to explore the other types of data sources that AppSync provides. This flexibility is part of the attraction to why AppSync is so widely recommended when building modern fullstack applications.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679432782644/b719b58e-3f87-47ac-bb44-cd6d0ee41c55.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-creating-appsync-functions">Creating AppSync Functions</h4>
<p>Perhaps one of the most compelling reasons to use AppSync is how it handles resolvers.</p>
<p>In AppSync, your resolver logic is put into a pipeline. This pipeline consists of small operations (known as functions) that perform one piece of logic.</p>
<blockquote>
<p>🗒️ We'll be talking about AppSync "functions" in this section quite a bit. It's important to know that these are <strong>not</strong> the same as Lambda functions.</p>
<p>Yes--these functions will be in JavaScript</p>
<p>Yes-- they will look like Lambda functions.</p>
<p>But they're not. <strong>In AppSync, a Lambda function is a data source</strong>, just like a DynamoDB table. <strong>An AppSync function is simply a step in a resolver,</strong> similar to promise-chaining with <code>.then</code></p>
</blockquote>
<p>If the idea of a pipeline resolver sounds fuzzy, check out this blog post I wrote that talks more about it.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.focusotter.cloud/the-anatomy-of-an-aws-appsync-pipeline-resolver">https://blog.focusotter.cloud/the-anatomy-of-an-aws-appsync-pipeline-resolver</a></div>
<p> </p>
<p>To instill this concept, let's create our first AppSync function. Just under where we created our data source, add the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> createTripFunction = <span class="hljs-keyword">new</span> awsAppsync.AppsyncFunction(
  scope,
  <span class="hljs-string">'createTripFunction'</span>,
  {
    name: <span class="hljs-string">'createTripFunction'</span>,
    api,
    dataSource: TripDataSource,
    runtime: awsAppsync.FunctionRuntime.JS_1_0_0,
    code: awsAppsync.Code.fromAsset(
      path.join(__dirname, <span class="hljs-string">'graphql/functions/Mutation.createTrip.js'</span>)
    ),
  }
)
</code></pre>
<p>Here we're creating an AppSync function that will be used for creating a <code>Trip</code>. This function is tied to the data source we created above. The function itself can be written in JavaScript or an older language known as VTL.</p>
<p>New applications shouldn't be using VTL and should opt for JavaScript, especially because the team will be releasing TypeScript support soon🤫</p>
<p>For a whirlwind tour of why this is a big deal, checkout the video below.</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/focusotter/status/1614040726320418816?s=20">https://twitter.com/focusotter/status/1614040726320418816?s=20</a></div>
<p> </p>
<p>The <code>code</code> argument is where we'll write our AppSync logic.</p>
<p>Just to show how reproducible it is to create an AppSync function, let's create a function for getting <code>Trip</code> data from the table. Under our previous function, paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> getTripFunction = <span class="hljs-keyword">new</span> awsAppsync.AppsyncFunction(
  scope,
  <span class="hljs-string">'getTripFunction'</span>,
  {
    name: <span class="hljs-string">'getTripFunction'</span>,
    api,
    dataSource: TripDataSource,
    runtime: awsAppsync.FunctionRuntime.JS_1_0_0,
    code: awsAppsync.Code.fromAsset(
      path.join(__dirname, <span class="hljs-string">'graphql/functions/Query.getTrip.js'</span>)
    ),
  }
)
</code></pre>
<p>Feel free to create the code for the<code>getUserFunction</code>, <code>updateTripFunction</code>, <code>deleteTripFunction</code>, and the <code>listTripsFunction</code> since they all follow the same format but have changed file paths and appropriate datasources.</p>
<h4 id="heading-creating-appsync-pipeline-resolvers">Creating AppSync Pipeline Resolvers</h4>
<p>As mentioned a couple of times now, a pipeline resolver is what gets triggered when a user makes a request to our API. So when we create our resolver, we have to tell what type and field to listen for.</p>
<p>To accomplish that, just beneath our AppSync functions, paste the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">new</span> awsAppsync.Resolver(scope, <span class="hljs-string">'createTripPipelineResolver'</span>, {
  api,
  typeName: <span class="hljs-string">'Mutation'</span>,
  fieldName: <span class="hljs-string">'createTrip'</span>,
  code: awsAppsync.Code.fromAsset(
    path.join(__dirname, <span class="hljs-string">'graphql/functions/passThrough.js'</span>)
  ),
  runtime: awsAppsync.FunctionRuntime.JS_1_0_0,
  pipelineConfig: [createTripFunction],
})
</code></pre>
<p>Recall a pipeline has a special <code>before</code> and <code>after</code> step. The <code>before</code> step allows us to perform logic (such as calculate values based on the arguments), while the <code>after</code> step allows us to perform additional formatting before returning the response to the user. We won't do either of those in this series, so I'm creating one JavaScript file that we'll reuse across all of our resolvers.</p>
<p>This pipeline resolver maps to our <code>Mutation.createTrip</code> operation and will call <code>createTripFunction</code>.</p>
<blockquote>
<p>🗒️ In case you're wondering why the <code>pipelineConfig</code> accepts an array of functions, consider a scenario where we are creating an order from <code>Stripe</code>.</p>
<p>The first function would be to get the Stripe secret, if that was successful, then it would pass the secret to another function that would call the Stripe API.</p>
<p>Not only does this separate concerns, but it allows us to reuse the <code>fetchStripeSecret</code> function in other pipelines.</p>
</blockquote>
<p>As before, once the steps are realized on how to create one resolver, creating another one becomes trivial. Let's create a resolver for the <code>Query.getTrip</code> operation by pasting in the following beneath the previous one:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">new</span> awsAppsync.Resolver(scope, <span class="hljs-string">'getTripPipelineResolver'</span>, {
  api,
  typeName: <span class="hljs-string">'Query'</span>,
  fieldName: <span class="hljs-string">'getTrip'</span>,
  code: awsAppsync.Code.fromAsset(
    path.join(__dirname, <span class="hljs-string">'graphql/functions/passThrough.js'</span>)
   ),
  runtime: awsAppsync.FunctionRuntime.JS_1_0_0,
  pipelineConfig: [getTripFunction],
})
</code></pre>
<p>Due to the steps being so similar, create the resolvers for the <code>Query.getUser</code>, <code>Mutation.deleteTrip</code>, <code>Mutation.updateTrip</code>, and the <code>Query.listTrips</code> operations. <strong>Make sure they each reference their respective</strong> <code>pipelineConfig</code> <strong>function.</strong></p>
<blockquote>
<p>🗒️ While it's important to remember we are defining our infrastructure, as opposed to creating an application, associating resolvers with functions can be a bit verbose.</p>
<p>For that reason, the AppSync team is looking into ways to condense the process of creating a resolver with only one function.</p>
</blockquote>
<h3 id="heading-defining-appsync-pipeline-functions">Defining AppSync Pipeline functions</h3>
<p>In this and the next section, we're going to write the logic for all of our resolvers. This is one of the areas that sets AppSync apart from other GraphQL providers the most and why it's such a compelling reason to build a modern API with AppSync.</p>
<h4 id="heading-passthrough-pipeline">PassThrough Pipeline</h4>
<p>Before talking about the functions, let's see an example.</p>
<p>In <code>lib/api/graphql/functions</code> create a file called <code>passThrough.js</code> and paste in the following:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// The before step.</span>
<span class="hljs-comment">// This runs before ALL the AppSync functions in this pipeline.</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-built_in">console</span>.log(ctx.args)
    ctx.stash.someValue = <span class="hljs-number">42</span>
    <span class="hljs-keyword">return</span> {}
}

<span class="hljs-comment">// The after step.</span>
<span class="hljs-comment">// This runs after ALL the AppSync functions in this pipeline.</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>{
    <span class="hljs-keyword">return</span> ctx.prev.result
}
</code></pre>
<p>The above function is the <code>before</code> and <code>after</code> state in our pipeline. Notice how the <code>request</code> takes in an object that contains the <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference-js.html">context of the request</a>. It also stores a value on the <code>stash</code> object called <code>someValue</code>. This allows functions further in pipeline to pull this value, without the value having to be passed down each through each function. Whatever the next function in line is, has to receive <em>something</em>. So we send an empty object.</p>
<p>The <code>response</code> also takes in <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference-js.html">its own context value</a> and in this case returns the <code>ctx.prev.result</code>, which is whatever the previous function sent to it.</p>
<blockquote>
<p>🗒️ What may clear things up is knowing that the <code>request</code> and <code>response</code> functions get transpiled by AppSync into VTL mapping templates. In VTL there is a request and response template.</p>
<p>This is why the <code>request</code> and <code>response</code> functions above can't be named anything else, and why they can't use features in JavaScript like <code>fetch</code>, or <code>fs</code>.</p>
<p>Going forward, I'll refer to the <code>request</code> and <code>response</code> functions as "mapping templates" or "templates" to make that point clear.</p>
</blockquote>
<h4 id="heading-createtrip-function">createTrip Function</h4>
<p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html#js-aws-appsync-resolver-reference-dynamodb-putitem">link to docs</a></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// lib/api/graphql/functions/Mutation.createTrip.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">let</span> id = util.autoId()
    <span class="hljs-keyword">let</span> values = ctx.args.input

    <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">operation</span>: <span class="hljs-string">'PutItem'</span>,
        <span class="hljs-attr">key</span>: util.dynamodb.toMapValues({ id }),
        <span class="hljs-attr">attributeValues</span>: util.dynamodb.toMapValues({
            <span class="hljs-attr">__typename</span>: <span class="hljs-string">'Trip'</span>,
            <span class="hljs-attr">createdAt</span>: util.time.nowISO8601(),
            <span class="hljs-attr">updatedAt</span>: util.time.nowISO8601(),
            ...values,
        }),
    }
}
<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>Based on our schema, we know the arguments that are optional and which ones are required. Since we know that, we use one of <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/built-in-util-js.html">the built-in <code>util</code> methods</a> to create a random <code>id</code> and capture the arguments that we were passed as <code>input</code>.</p>
<p>From there, we call <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html#js-aws-appsync-resolver-reference-dynamodb-putitem">the <code>PutItem</code> operation</a> to create store an item in our table. AppSync knows which table we're working with because we assigned it as our data source.</p>
<blockquote>
<p>🗒️ The <code>@aws-appsync/utils</code> library is preinstalled by the AppSync runtime so it's not necessary to install, however, it's nice to get the intellisense when developing.</p>
<p>To add the run library, run the following from the root of your project:</p>
<p><code>npm i @aws-appsync/utils</code></p>
</blockquote>
<p>In the response template, we take the data that we receive from the DynamoDB (the newly created object in our Database, and forward it to our next step--the <code>after</code> step in our pipeline.</p>
<p>That's the complete flow of how data moves through our application.</p>
<p>For the next functions, I'll share a link to the docs, and point out any areas worth mentioning, but they will largely consist of the code for the mapping templates themselves.</p>
<h4 id="heading-deletetrip-function">deleteTrip Function</h4>
<p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html#js-aws-appsync-resolver-reference-dynamodb-deleteitem">link to docs</a></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// lib/api/graphql/functions/Mutation.deleteTrip.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">return</span> {
        <span class="hljs-attr">operation</span>: <span class="hljs-string">'DeleteItem'</span>,
        <span class="hljs-attr">key</span>: util.dynamodb.toMapValues({ <span class="hljs-attr">id</span>: ctx.args.id }),
    }
}

<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>
<h4 id="heading-updatetrip-function">updateTrip Function</h4>
<p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html#js-aws-appsync-resolver-reference-dynamodb-updateitem">link to docs</a></p>
<p>Note that both the <code>updateTrip</code> and <code>createTrip</code> both use the <code>PutItem</code> operation. The difference is that when creating a <code>Trip</code> there is no <code>id</code> to pass in since it's created automatically.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// lib/api/graphql/functions/Mutation.updateTrip.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">let</span> { id, ...values } = ctx.args.input

    <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">operation</span>: <span class="hljs-string">'PutItem'</span>,
        <span class="hljs-attr">key</span>: util.dynamodb.toMapValues({ id }),
        <span class="hljs-attr">attributeValues</span>: util.dynamodb.toMapValues({
            <span class="hljs-attr">__typename</span>: <span class="hljs-string">'Trip'</span>,
            <span class="hljs-attr">updatedAt</span>: util.time.nowISO8601(),
            ...values,
        }),
    }
}

<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>
<h4 id="heading-gettrip-function">getTrip Function</h4>
<p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html#js-aws-appsync-resolver-reference-dynamodb-getitem">link to docs</a></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// lib/api/graphql/functions/Query.getTrip.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">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.args.id }),
    }
}

<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>
<h4 id="heading-listtrips-function">listTrips Function</h4>
<p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html#js-aws-appsync-resolver-reference-dynamodb-scan">link to docs</a></p>
<p>While not shown in this example, with the <code>scan</code> operation it's possible to perform token-based pagination.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// lib/api/graphql/functions/Query.listTrips.js</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>  {
        <span class="hljs-attr">operation</span>: <span class="hljs-string">'Scan'</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>{
    <span class="hljs-keyword">const</span> response = ctx.result.items

    <span class="hljs-keyword">return</span> response
}
</code></pre>
<h4 id="heading-getuser-function">getUser Function</h4>
<p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html#js-aws-appsync-resolver-reference-dynamodb-getitem">link to docs</a></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// lib/api/graphql/functions/Query.getUser.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">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.args.id }),
    }
}

<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>
<hr />
<h4 id="heading-a-bit-of-honesty">A Bit of Honesty</h4>
<p>I'll be upfront because I know some of you like to poke around. Currently, each AppSync function does take a <code>requestMappingTemplate</code> and a <code>responseMappingTemplate</code> key. Further, the AppSync construct does have a <code>MappingTemplate</code> property that has full DynamoDB support for CRUD operations:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679451661201/c19b6489-9fff-4160-bf59-17c7b5404bda.png" alt class="image--center mx-auto" /></p>
<p>However, while those methods may be nice convenience APIs in the beginning. I wanted to show that writing JavaScript files, while verbose, is more valuable due to learning about how things are mapped under the hood.</p>
<blockquote>
<p>🗒️ I would much rather have you refactor to something simpler and know how it works, than try to scale up and not understand it.</p>
</blockquote>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this chapter, we dove <em>deep</em> into building out AppSync APIs. Not only did we cover schemas, directives, resolvers and data sources, but we also set up create, read, update, delete, and list functionality (with support for WebSockets) for an API in a ready-to-deploy, ready-to-modify way!</p>
<p>That's no small feat!</p>
<p>We also talked about AppSync functions and how they fit as pieces in a pipeline.</p>
<p>This was the last piece to this stack. In the next chapter, we'll talk all about deployment!</p>
<p>Until then, Happy Coding</p>
<p>🦦</p>
]]></content:encoded></item><item><title><![CDATA[AWS CDK for Frontend Developers: Amazon S3 and CloudFront]]></title><description><![CDATA[If there was ever a service that intimidated me when first learning AWS, it was Amazon Simple Storage Service (S3). This is a service where you can upload a virtually unlimited amount of data into an S3 bucket and each item can be as large as 5 terab...]]></description><link>https://blog.focusotter.com/aws-cdk-for-frontend-developers-amazon-s3-and-cloudfront</link><guid isPermaLink="true">https://blog.focusotter.com/aws-cdk-for-frontend-developers-amazon-s3-and-cloudfront</guid><category><![CDATA[AWS]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Startups]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Mon, 20 Mar 2023 10:24:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679291444881/f6fdef5b-6863-4e01-8293-5034af7d9810.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If there was ever a service that intimidated me when first learning AWS, it was Amazon Simple Storage Service (S3). This is a service where you can upload a virtually unlimited amount of data into an S3 bucket and each item can be as large as 5 <strong>terabytes</strong> in size.</p>
<p>However, it's also where I heard the horror stories of big bills due to random file uploads, assets being taken due to misconfigured permission and other aspects that scared me to be very happy to either pay for some 3rd-party service or settle with their limitations.</p>
<p>Reflecting on it now, it was never S3 keeping me away from using it, but my lack of knowledge about roles and policies in AWS, as well as CORS.</p>
<p>So in this post, not only will we see firsthand how simple creating an S3 bucket with a CDN is, but we'll discuss the security as well.</p>
<p>By the end of this chapter, I hope you'll be more confident and better prepared to work with permissions in your own applications by having a strong grasp on how they apply in our Trip Logger app.</p>
<h2 id="heading-great-power">Great Power</h2>
<h3 id="heading-an-introduction-to-amazon-s3">An Introduction to Amazon S3</h3>
<p>As mentioned before, Amazon S3 is a storage service capable of storing practically any type of data. This could be PDF, CSV, images, JSON, or anything else.</p>
<blockquote>
<p>🗒️ <strong>S3 is not a database</strong></p>
<p>When you need to perform database operations like querying, sorting, and filtering data, it's not efficient to store that data in S3. S3 is designed to store and retrieve objects, like files, and accessing data in S3 requires downloading the entire object</p>
</blockquote>
<p>In our Trip Logger application, we'll implement both reading and writing data into our S3 bucket. For reading, the viewer doesn't have to be logged in.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679162264102/3005177c-2129-4e2e-95c1-74d2f04420da.png?auto=compress,format&amp;format=webp" alt="homepage with items" /></p>
<hr />
<p>For writing into our bucket, the user will have to be authenticated.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679162324710/7dac324c-3863-4dd7-b5fc-3d7d73661d97.png?auto=compress,format&amp;format=webp" alt="protected page to create a trip item" /></p>
<hr />
<h3 id="heading-understanding-s3-access-levels">Understanding S3 access levels</h3>
<p>It's important to note, as seen in our architecture diagram below, that when it comes specifically to our S3 bucket, all requests are going through Cognito even if they aren't signed in. To understand this, we have to think about what it means to be <em>unauthenticated.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679156038320/313068be-b268-4a85-9167-4e8f4019030e.png?w=1600&amp;h=840&amp;fit=crop&amp;crop=entropy&amp;auto=compress,format&amp;format=webp" alt="AWS CDK for Frontend Developers: The Ultimate Guide to Get You Started" /></p>
<p>When it comes to items stored in an S3 bucket, there are 3 essential states to consider:</p>
<ol>
<li><p><strong>Anonymous Access</strong>: Users who view our files on a different domain than our application.</p>
</li>
<li><p><strong>Guest Access</strong>: Users who view our files on our domain, but have not authenticated.</p>
</li>
<li><p><strong>Authenticated Access</strong>: Users who have signed into our application.</p>
</li>
</ol>
<p>Using an e-commerce application as an example, guest access would be provided when a customer visits our site. They should have the ability to read public files without having to authenticate. However, the store owner may be able to log into the site and create, read, update, and delete products based on availability.</p>
<p>In addition, when a customer decides to purchase items, they may be taken away from our site, to a payment processor like <a target="_blank" href="https://stripe.com/">Stripe</a>. Stripe will need to read our product images from their domain instead of our own. This is an example of anonymous access.</p>
<p><img src="https://d2908q01vomqb2.cloudfront.net/0a57cb53ba59c46fc4b692527a38a87c78d84028/2022/11/21/stripe-checkout.png" alt="stripe-checkout" /></p>
<p>With those concepts in place, let's create our first S3 bucket.</p>
<h3 id="heading-creating-a-simple-s3-bucket">Creating a Simple S3 Bucket</h3>
<p>Create a new file called <code>lib/s3/tripPics.ts</code> and paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { envNameContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'./../../cdk.context.d'</span>
<span class="hljs-keyword">import</span> { Construct } <span class="hljs-keyword">from</span> <span class="hljs-string">'constructs'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> s3 <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-s3'</span>

<span class="hljs-keyword">type</span> CreateTripPicsBucketProps = {
    appName: <span class="hljs-built_in">string</span>
    env: envNameContext
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createTripPicsBucket</span>(<span class="hljs-params">
    scope: Construct,
    props: CreateTripPicsBucketProps
</span>) </span>{
    <span class="hljs-keyword">const</span> fileStorageBucket = <span class="hljs-keyword">new</span> s3.Bucket(
        scope,
        <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-bucket`</span>,
        {}
    )

    <span class="hljs-keyword">return</span> fileStorageBucket
}
</code></pre>
<p>Skipping over the concepts we've already gone over, you'll see that the L2 construct for creating an S3 bucket simply takes in scope, an ID as the required props. When it comes to the configuration object, it doesn't require any so it's an empty object.</p>
<blockquote>
<p>🗒️ Worth noting is there is a <code>bucketName</code> field that we can pass in. However, it is best practice to let CloudFormation (via the CDK) generate this name for you.</p>
<p>This is due to S3 bucket names needing to be <strong>globally</strong> unique.</p>
</blockquote>
<p>In our <code>lib/backend-trip-post-stack.ts</code> file, call out <code>createTripPicsBucket</code> function by pasting in the following underneath our database references:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> tripPicsBucket = createTripPicsBucket(<span class="hljs-built_in">this</span>, {
    appName: context.appName,
    env: context.environment,
})
</code></pre>
<p>That's it! You've now created an S3 bucket. While this is great, for us to make any use of it in our application, we'll have to apply some permissions to it.</p>
<h2 id="heading-great-responsibility">Great Responsibility</h2>
<h3 id="heading-s3-bucket-permissions">S3 Bucket Permissions</h3>
<blockquote>
<p>"With great power, comes great responsibility."</p>
<p>- Uncle Ben🕷️</p>
</blockquote>
<p>So far in our Trip Logger application, we had a brief exposure to policies when we gave our Lambda function permission to write data to our User table. However, for our bucket, it's important to understand the permissions we're setting and not rely on those methods.</p>
<p>To accomplish that, let's first talk about a friend we all know and love:</p>
<h4 id="heading-cors">CORS</h4>
<p><img src="https://imgs.search.brave.com/gb1QMccK6w0laXwAK6D6xNZ9LBolkFot34Ap06xnL8A/rs:fit:1200:184:1/g:ce/aHR0cHM6Ly9taXJv/Lm1lZGl1bS5jb20v/bWF4LzE0MDAvMCpi/STJ5eEtyeXFKenlV/a3Vk" alt="3 Ways to Fix the CORS Error — and How the Access-Control-Allow-Origin Header Works | by David ..." /></p>
<p>Cross-origin-resource-sharing (CORS) is the bane of many frontend developers.</p>
<p>By default, a server can talk to another server. However, a browser can only talk to a server if that server has allowed specific methods and headers saying it's ok to do so.</p>
<p>As frontend developers, we know this--and yet, I still run into this issue to this day.</p>
<p>With regards to our S3 bucket, it lives on an AWS server somewhere. We would like to read, update, and delete images from that bucket from our browser. To do that, we have to enable CORS on our bucket.</p>
<p>Fortunately, our empty config object on our bucket has a <code>cors</code> property that we can add. Back in our <code>lib/s3/tripPics.ts</code> file, paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createTripPicsBucket</span>(<span class="hljs-params">
    scope: Construct,
    props: CreateTripPicsBucketProps
</span>) </span>{
    <span class="hljs-keyword">const</span> fileStorageBucket = <span class="hljs-keyword">new</span> s3.Bucket(
        scope,
        <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-bucket`</span>,
        {
            <span class="hljs-comment">// the new part</span>
            cors: [
                {
                    allowedMethods: [
                        s3.HttpMethods.GET,
                        s3.HttpMethods.POST,
                        s3.HttpMethods.PUT,
                        s3.HttpMethods.DELETE,
                    ],
                    allowedOrigins: props.allowedOrigins,
                    allowedHeaders: [<span class="hljs-string">'*'</span>],
                    exposedHeaders: [
                        <span class="hljs-string">'x-amz-server-side-encryption'</span>,
                        <span class="hljs-string">'x-amz-request-id'</span>,
                        <span class="hljs-string">'x-amz-id-2'</span>,
                        <span class="hljs-string">'ETag'</span>,
                    ],
                },
            ],
        }
    )

    <span class="hljs-keyword">return</span> fileStorageBucket
}
</code></pre>
<ul>
<li><p><code>allowedMethods</code>: A list of predefined HTTP methods. It's important to note, this is just saying what's possible to do from the browser. The actual user authorization permission will be handled later.</p>
</li>
<li><p><code>allowedOrigins</code>: An array of domains that can perform the above <code>allowedmethods</code>.</p>
</li>
<li><p><code>allowedHeaders</code>: An array containing headers that the frontend can pass to the S3. By specifying <code>*</code>, we are allowing all headers to be passed.</p>
</li>
<li><p><code>exposedHeaders</code>: The headers S3 sends to the client.</p>
</li>
</ul>
<p>While not important to understand, simply for those curious, here's a bit on what the exposed header values relate to:</p>
<ul>
<li><p><code>x-amz-server-side-encryption</code>: Whether the object in S3 is stored using server-side encryption, and if so, which encryption algorithm was used</p>
</li>
<li><p><code>x-amz-request-id</code>: A unique identifier for the request made to S3, which can be used for troubleshooting purposes</p>
</li>
<li><p><code>x-amz-id-2</code>: A string that identifies the Amazon S3 bucket that processed the request</p>
</li>
<li><p><code>ETag</code>: A unique identifier for the version of the object stored in S3, which can be used for caching purposes and to determine if the object has been modified</p>
</li>
</ul>
<blockquote>
<p>🗒️ I'll admit, I cheated a bit here. I know that we'll be using the AWS Amplify JS libraries for S3 image handling on our frontend, and they have a section dedicated to <a target="_blank" href="https://docs.amplify.aws/lib/storage/getting-started/q/platform/js/#amazon-s3-bucket-cors-policy-setup">supplying the CORS configuration</a>.</p>
</blockquote>
<p>The <code>allowedOrigins</code> field is handled by props because the origin will change depending on if we are on our <code>develop</code> environment, or <code>production</code>.</p>
<p>To include that field in our props, ensure the <code>cdk.context.d.ts</code> file has:</p>
<pre><code class="lang-typescript">s3AllowedOrigins: [<span class="hljs-built_in">string</span>]
</code></pre>
<p>Likewise, in the <code>environments</code> array, add the following to the object:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"s3AllowedOrigins"</span>: [<span class="hljs-string">"http://localhost:3000"</span>],
</code></pre>
<p>By now, TypeScript should be complaining in our stack that the props are missing. Update the relevant files to contain the needed props and values.</p>
<h4 id="heading-understanding-roles-and-policies">Understanding Roles and Policies</h4>
<p>A crucial part of working with AWS is understanding roles and permissions. These are managed in an <a target="_blank" href="https://aws.amazon.com/iam/">AWS service called IAM</a>, as such, you'll often hear "IAM Role" or "IAM Policy".</p>
<p>While this is worthy of its own series, the simple explanation is this:</p>
<p>A <strong>policy</strong> in AWS is a rule that says who can do what. "Who" in this context can be an AWS user or another AWS service.</p>
<p>A <strong>role</strong> is a set of policies that describe who the user will become if that role is applied to them.</p>
<p>A common metaphor is that a role is a hat. If you put on a construction hat, you inherit all of the authority of a construction worker. If you take the hat off and put on a captain's hat, you now have full authority to perform all the duties of a captain.</p>
<blockquote>
<p>🗒️ The subtle, but important, concept here is that only one hat can be put on at a time. 🎩👒⛑️</p>
</blockquote>
<h4 id="heading-granting-access-to-our-s3-bucket-through-iam-policies">Granting Access to our S3 Bucket Through IAM Policies</h4>
<p>The first policy we'll create is a common one: allowing <code>read</code> access to our bucket.</p>
<p>In our <code>lib/s3/tripPics.ts</code> file, past the following before the <code>return</code> statement:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> allowReadFromBucket = <span class="hljs-keyword">new</span> iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    principals: [<span class="hljs-keyword">new</span> StarPrincipal()],
    actions: [<span class="hljs-string">'s3:GetObject'</span>],
    resources: [<span class="hljs-string">`<span class="hljs-subst">${fileStorageBucket.bucketArn}</span>/public/*`</span>],
})
</code></pre>
<p>At the top of the file, also include the following imports:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> iam <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-iam'</span>
<span class="hljs-keyword">import</span> { StarPrincipal } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-iam'</span>
</code></pre>
<p>As you can see there is an L2 construct for easily creating IAM policies. The <code>StarPrincipal</code> used to say "anyone" in an policy.</p>
<p>In plain speak, this policy reads as, "Allow anyone to get an object from an S3 bucket--specifically, any object from the public folder of the fileStorageBucket."</p>
<blockquote>
<p>🗒️ I purposely put the policy keys in this order so that it read like that. Because it's just a JavaScript object, the order doesn't actually matter, but I find it helps when first learning.</p>
</blockquote>
<p>Worth talking about is the <code>fileStorageBucket.bucketArn</code> line. Every AWS service has a unique identifier known as an Amazon Resource Name (ARN). In our case, we appended the <code>/public/*</code> part to further scope down access to a folder and not the entire bucket.</p>
<p>Now that we have the policy to read from our bucket, let's add another for signed-in users:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Let signed in users CRUD on a bucket</span>
<span class="hljs-keyword">const</span> canReadUpdateDeleteFromPublicDirectory = <span class="hljs-keyword">new</span> iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: [<span class="hljs-string">'s3:PutObject'</span>, <span class="hljs-string">'s3:GetObject'</span>, <span class="hljs-string">'s3:DeleteObject'</span>],
    resources: [<span class="hljs-string">`<span class="hljs-subst">${fileStorageBucket.bucketArn}</span>/public/*`</span>],
})
</code></pre>
<p>As you can see, it's similar to the first one, but has more permission allowed. Additionally, we removed the IAM principal since in the next section we'll promote this policy to a "managed policy".</p>
<h4 id="heading-adding-iam-policies-to-iam-roles">Adding IAM Policies to IAM Roles</h4>
<p>Think back, or look up, at our architecture diagram. Then try to answer the following question: We are creating policies that allow access to our S3 bucket, but for who?</p>
<p>Remember when I said the "who" can be a user or an AWS service?</p>
<p>We are providing access to our Amazon Cognito identity pool.</p>
<p>Just under our previous policy, paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">new</span> iam.ManagedPolicy(
    scope,
    <span class="hljs-string">'SignedInUserManagedPolicy'</span>,
    {
        description:
            <span class="hljs-string">'Allow access to s3 bucket by signed in users.'</span>,
        statements: [canReadUpdateDeleteFromPublicDirectory],
        roles: [props.authenticatedRole],
    }
)
</code></pre>
<p>AWS comes with a set of policies. When you first created your first AWS user, you likely used one: <code>AWSAdminstratorFullAccess</code></p>
<p>The distinction is small, but what's important to note is that most AWS services will let you give them a regular policy as we defined earlier, but some require an AWS-managed policy and this is how we create one. Not only does it accept an array of policies, but it also accepts an array of roles for us to apply.</p>
<p>In our case, this role is the <code>authenticated</code> state of our Cognito identity pool. That's to say when a user makes a request, for one of our services, as part of the request, they will pass information about their current auth state (via a header). If they are authenticated, then this role will be invoked.</p>
<p>To pass the role from our identity pool, first, update the <code>CreateTripPicsBucketProps</code> in this file so it's expected:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> CreateTripPicsBucketProps = {
    appName: <span class="hljs-built_in">string</span>
    env: envNameContext
    allowedOrigins: [<span class="hljs-built_in">string</span>]
    authenticatedRole: iam.IRole
}
</code></pre>
<p>Next, in our <code>lib/backend-trip-post.stack.ts</code> file, simply pass the role from the identity pool to the bucket as such:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> tripPicsBucket = createTripPicsBucket(<span class="hljs-built_in">this</span>, {
    appName: context.appName,
    env: context.environment,
    allowedOrigins: context.s3AllowedOrigins,
    authenticatedRole: cognitoAuth.identityPool.authenticatedRole,
})
</code></pre>
<p>Hopefully, it's starting to make sense at this point that much like frontend applications, the right level of abstraction and file organization can greatly pay off when it comes to passing values around!</p>
<h3 id="heading-improving-content-delivery-with-amazon-cloudfront">Improving Content Delivery with Amazon CloudFront</h3>
<p>You may be wondering why we didn't also create a managed policy for the <code>unauthenticated</code> role provided by our Cognito identity pool.</p>
<p>The reason is that we're not going to allow direct access to our S3 bucket at all.</p>
<p>The first policy we created was simply to get us comfortable working with policies. We won't actually be using it and instead will have our users access images from a content delivery network (CDN).</p>
<h4 id="heading-getting-started-with-a-content-delivery-network-cdn">Getting started with a Content Delivery Network (CDN)</h4>
<p>When we request a file from S3, that image has to be retrieved from an AWS server and is then loaded into our browser. Now, browsers may opt to do some caching, but it's only for your personal benefit. If your neighbor wants to see that same image, they would have to make that full network trip as well, even though you already brought the image next door.</p>
<p>A CDN solves that by caching the image at the nearest AWS network location near you. Now, when your neighbor requests that image you've cached it closer to you, so they get a faster response time.</p>
<p><img src="https://imgs.search.brave.com/9qvP5mw1jPbDMrKpdo1MCvu2t-SydYj3vEIaae9UylM/rs:fit:1000:454:1/g:ce/aHR0cHM6Ly93aXNo/ZGVzay5jb20vc2l0/ZXMvZGVmYXVsdC9m/aWxlcy9pbmxpbmUt/aW1hZ2VzL3doeS11/c2UtY2RuLmpwZw" alt="How to speed up WordPress site | WishDesk" /></p>
<p>An added benefit is that we don't need to allow public read access to our bucket since our CDN will be sitting between our bucket and our users.</p>
<p>To create our CDN, in the <code>lib/s3/tripPics.ts</code> file, add the following imports:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> awsCloudfront <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-cloudfront'</span>
<span class="hljs-keyword">import</span> { S3Origin } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-cloudfront-origins'</span>
</code></pre>
<p>Next, replace the IAM policy that just allowed <code>Get:Object</code> access with the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> fileStorageBucketCFDistribution = <span class="hljs-keyword">new</span> awsCloudfront.Distribution(
    scope,
    <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-CDN`</span>,
    {
        defaultBehavior: {
            origin: <span class="hljs-keyword">new</span> S3Origin(fileStorageBucket, { originPath: <span class="hljs-string">'/public'</span> }),
            cachePolicy: awsCloudfront.CachePolicy.CACHING_OPTIMIZED,
            allowedMethods: awsCloudfront.AllowedMethods.ALLOW_GET_HEAD,
            cachedMethods: awsCloudfront.AllowedMethods.ALLOW_GET_HEAD,
            viewerProtocolPolicy:
                awsCloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        },
    }
)
</code></pre>
<p>In addition to setting caching methods and redirecting HTTPS, the important part here is:</p>
<ol>
<li><p>The <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront-readme.html">L2 CloudFront construct</a> makes this really simple</p>
</li>
<li><p>The line <code>origin: new S3Origin(fileStorageBucket, { originPath: '/public' })</code></p>
</li>
</ol>
<p>When our CloudFront distribution deploys, it'll generate a URL for us. This will directly map to our S3 bucket. However, we only want to map to the <code>/public</code> folder.</p>
<p>By setting the <code>originPath</code>, a request to <code>my-cdn.com/image.jpg</code> will now forward to our S3 bucket: <code>my-s3-bucket/public/image.jpg</code>.</p>
<p>Lastly, because we are introducing two new services that will be used by our frontend, let's make sure to return them both.</p>
<p>Update the <code>return</code> statement to the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">return</span> { fileStorageBucket, fileStorageBucketCFDistribution }
</code></pre>
<blockquote>
<p>🗒️ <strong>A note on pricing</strong></p>
<p><a target="_blank" href="https://aws.amazon.com/s3/pricing/">For S3 pricing</a>, after the 1-year free tier period, the cost of storing your first 50 terabytes of data in the us-east-1 region is $0.023 per GB. There are also data retrieval costs at $0.0004/1000 GET requests and $0.005/1000 PUT or DELETE requests.</p>
<p><a target="_blank" href="https://aws.amazon.com/cloudfront/pricing/">For Amazon CloudFront pricing</a>, their always-free tier includes the following:</p>
<ul>
<li><p>1 TB of data transfer out per month</p>
</li>
<li><p>10,000,000 HTTP or HTTPS Requests per month</p>
</li>
<li><p>2,000,000 CloudFront Function invocations per month</p>
</li>
<li><p>Free SSL certificates</p>
</li>
<li><p>No limitations, all features available</p>
</li>
</ul>
</blockquote>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this post, we created an Amazon S3 bucket and an Amazon CloudFront distribution. My hope is what came across in this chapter is that creating an S3 bucket and a CDN took a relatively small amount of code and that the real focus was understanding policies and roles.</p>
<p>IAM configurations take time and practice. Fortunately, we'll continue to touch on them throughout this series. However, with what we created today, you now have the tools to create your own buckets with many best practices in mind.</p>
<p>In the next chapter, we'll bring our backend services together by creating a modern, full-managed, API that is not only completely serverless, but also completely functionless.</p>
<p>Until then, happy coding!</p>
<p>🦦</p>
]]></content:encoded></item><item><title><![CDATA[The AWS CDK Guide to Authentication and Authorization for Frontend Developers]]></title><description><![CDATA[When it comes to learning AWS, it's important to keep in mind that what sets your product apart from the rest is all of the pieces that are different. The parts that are true for any app are what AWS creates services for. In AWS land, this is known a...]]></description><link>https://blog.focusotter.com/the-aws-cdk-guide-to-authentication-and-authorization-for-frontend-developers</link><guid isPermaLink="true">https://blog.focusotter.com/the-aws-cdk-guide-to-authentication-and-authorization-for-frontend-developers</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Startups]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Cloud]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Mon, 20 Mar 2023 04:58:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679289285892/2149a44f-75a0-4e79-b81d-f5308ad633be.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When it comes to learning AWS, it's important to keep in mind that what sets your product apart from the rest is all of the pieces that are different. The parts that are true for any app are what AWS creates services for. In AWS land, this is known as <em>undifferentiated heavy lifting.</em></p>
<p>Your customers don't care that you have a fancy homegrown solution for hashing and salting passwords. They just want a safe and secure way to log into your app. Not only is signing up for a service common but the complete handling of the user flow is also expected.</p>
<p>So far, in our application, we have two DynamoDB tables and a Lambda function. These are both examples of services that do one thing <em>really</em> well. However, some services offer multiple services under their name. <a target="_blank" href="https://aws.amazon.com/cognito/">Amazon Cognito</a> is such a service.</p>
<p>Cognito is comprised of three main services: a user pool, identity pool, and a user pool client.</p>
<p>Let's discuss each of those to see what they provide.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679203048053/8ddfd4fb-4411-4b8d-b23c-f15f3c50d471.png?auto=compress,format&amp;format=webp" alt /></p>
<h2 id="heading-authentication">Authentication</h2>
<p>Frontend developers use authentication to provide credentials that identify who they are. This is often done by providing an email and password, or by signing in with a social provider. Popular solutions like Auth0 and Auth.js are commonly used in the frontend/indie hacker community for their ease of use and developer experience.</p>
<p>When working with AWS services, the preferred solution is a Cognito userpool. This service is a directory or pool of users that contains information about the users in your application. It offers a complete sign-up experience, including sign up, sign in, sign out, password recovery, and more. It also allows for simple integration with Google and Facebook log-ins. The Amplify JavaScript libraries further simplify the authentication experience.</p>
<p>When a user signs up for an application using Cognito, a unique ID is generated for them, along with their email and username. While custom attributes can be added, they are generally expected to remain unchanged. This is because that information is stored in the JSON Web Token (JWT) provided by Cognito. To avoid users having to log out and log in to update their information, it's best to store user data in a User table.</p>
<blockquote>
<p>🗒️ The combination of Cognito -&gt; Lambda -&gt; DynamoDB is a common approach in both the serverless community and the SaaS community due to its flexibility, scalability, and robustness.</p>
</blockquote>
<h3 id="heading-creating-a-cognito-userpool-with-the-aws-cdk">Creating a Cognito Userpool with the AWS CDK</h3>
<p>In our Trip Logger CDK project, create a following in the <code>lib/cognito/auth.ts</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Construct } <span class="hljs-keyword">from</span> <span class="hljs-string">'constructs'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> awsCognito <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-cognito'</span>
<span class="hljs-keyword">import</span> { NodejsFunction } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-lambda-nodejs'</span>
<span class="hljs-keyword">import</span> { envNameContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../cdk.context'</span>

<span class="hljs-keyword">type</span> CreateTravelUserpool = {
    appName: <span class="hljs-built_in">string</span>
    env: envNameContext
    addUserPostConfirmation: NodejsFunction
}
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createTravelUserpool</span>(<span class="hljs-params">
    scope: Construct,
    props: CreateTravelUserpool
</span>) </span>{
<span class="hljs-comment">// the L2 Construct for a userpool</span>
<span class="hljs-keyword">const</span> userPool = <span class="hljs-keyword">new</span> awsCognito.UserPool(scope, <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-userpool`</span>, {
  userPoolName: <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-userpool`</span>,
  selfSignUpEnabled: <span class="hljs-literal">true</span>,
  accountRecovery: awsCognito.AccountRecovery.EMAIL_ONLY,
  userVerification: {
    emailStyle: awsCognito.VerificationEmailStyle.CODE,
  },
  standardAttributes: {
    email: {
      required: <span class="hljs-literal">true</span>,
      mutable: <span class="hljs-literal">true</span>,
    },
  },
})
    <span class="hljs-keyword">return</span> { userPool }
}
</code></pre>
<p>At this point in the series, I'll skip over the types and the function and instead focus on the <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPool.html">L2 Cognito construct</a> itself. From there we can discuss the following properties of our userpool:</p>
<ul>
<li><p><code>userPoolName</code>: The name of the user pool. This is a required field.</p>
</li>
<li><p><code>selfSignUpEnabled</code>: A boolean value that indicates whether users can sign themselves up for the user pool.</p>
</li>
<li><p><code>lambdaTriggers:</code> An object that allows us to hook into various lifecycles of a user authentication process and invoke a lambda function in response.</p>
</li>
<li><p><code>postConfirmation</code>: <a target="_blank" href="https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-confirmation.html">A lifecycle event</a> that is triggered after a user confirms their account registration.</p>
</li>
<li><p><code>accountRecovery</code>: Determines the account recovery method. In this case, <code>EMAIL_ONLY</code> is specified, which means that users can recover their accounts using only their email addresses.</p>
</li>
<li><p><code>userVerification</code>: An object that specifies the type of user verification. In this case, <code>emailStyle</code> is set to <code>awsCognito.VerificationEmailStyle.CODE</code>, which means that verification codes will be sent to users via email.</p>
</li>
<li><p><code>standardAttributes</code>: An object that specifies the standard attributes for the user pool. In this case, only the <code>email</code> attribute is set with <code>required</code> and <code>mutable</code> properties.</p>
</li>
<li><p><code>required</code>: When set to <code>true</code>, then the <code>email</code> attribute is required, and users must provide a value for this attribute during sign-up.</p>
</li>
<li><p><code>mutable</code>: Determines whether the <code>email</code> attribute can be updated by the user.</p>
</li>
</ul>
<h3 id="heading-creating-a-cognito-web-client-with-the-aws-cdk">Creating a Cognito Web Client with the AWS CDK</h3>
<p>Our userpool is just that--a place where we store users. Cognito doesn't know (or care) how our users get there and we have to tell it. To do that, we use the <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClient.html"><code>L2 Cognito UserPoolClient</code></a> to create a web client and pass it to our userpool.</p>
<p>Replace the <code>return {userpool}</code> code above with the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> userPoolClient = <span class="hljs-keyword">new</span> awsCognito.UserPoolClient(
    scope,
    <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-userpoolClient`</span>,
    { userPool }
)
<span class="hljs-keyword">return</span> {userpool, userPoolClient}
</code></pre>
<h2 id="heading-authorization">Authorization</h2>
<p>If authentication is a bouncer at a nightclub that checks your ID before getting in, then authorization is the V.I.P. access lounge that only certain people can get into.</p>
<p>That's to say, authentication is <em>who</em> you are, while authorization is <em>what</em> you are allowed to do.</p>
<p>Amazon Cognito handles authorization via identity pools.</p>
<p>When it comes to authorization, we're not just talking about the users that have authenticated with our application. This also applies to how <code>unauthenticated</code> users interact with our app, and by extension, AWS services.</p>
<p>This will make more sense in the next section where we add image support to our application with AWS S3.</p>
<h3 id="heading-creating-a-cognito-identity-pool-with-the-aws-cdk">Creating a Cognito Identity Pool with the AWS CDK</h3>
<p>For a service feature so powerful, actually creating an identity pool is a bit lackluster.</p>
<p>For the last time, replace the <code>return</code> statement in our function with the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> identityPool = <span class="hljs-keyword">new</span> IdentityPool(
  scope,
  <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-identityPool`</span>,
  {
    identityPoolName: <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>IdentityPool`</span>,
    allowUnauthenticatedIdentities: <span class="hljs-literal">true</span>,
    authenticationProviders: {
      userPools: [
        <span class="hljs-keyword">new</span> UserPoolAuthenticationProvider({
          userPool: userPool,
          userPoolClient: userPoolClient,
        }),
      ],
    },
  }
)
</code></pre>
<p>Aside from giving it a name, and passing in our userpool and client, the only thing to call out is that we explicitly tell our identity pool to allow unauthenticated users to access our AWS services. What those users can do is still fully up to us, but the ability to even attempt access is now provided.</p>
<p>However, you should notice at this point that TypeScript is unhappy. Specifically, it can't find the <code>IdentityPool</code> and <code>UserPoolAuthenticationProvider</code> constructs.</p>
<p>This is because these <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cognito-identitypool-alpha-readme.html">L2 constructs</a> are currently in alpha and haven't been merged into the main library yet.</p>
<blockquote>
<p>🗒️ As a word of caution, be careful when using alpha constructs as they can have breaking changes. I'm opting to use them in this series because well...I work at AWS and may or may not have knowledge that these specific features are fine to use for our purposes.</p>
</blockquote>
<p>To install the <code>alpha</code> package, run the following command:</p>
<pre><code class="lang-bash">npm i @aws-cdk/aws-cognito-identitypool-alpha
</code></pre>
<h2 id="heading-updating-our-stack-with-cognito">Updating our stack with Cognito</h2>
<p>As was the same in previous chapters, we'll invoke our <code>createTravelUserpool</code> function by passing in the <code>appName</code> , <code>env</code>and the <code>addUserPostConfirmation</code> props. This should be done just beneath the Lambda function we created in the previous chapter, but before our database (no reason in particular, I just like to keep related services together).</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// created in previous chapter</span>
<span class="hljs-keyword">const</span> addUserFunc = <span class="hljs-keyword">new</span> NodejsFunction(<span class="hljs-built_in">this</span>, <span class="hljs-string">'addUserFunc'</span>, {
    functionName: <span class="hljs-string">`<span class="hljs-subst">${context.appName}</span>-<span class="hljs-subst">${context.environment}</span>-addUserFunc`</span>,
    runtime: Runtime.NODEJS_16_X,
    handler: <span class="hljs-string">'handler'</span>,
    entry: path.join(__dirname, <span class="hljs-string">`./functions/addUser/main.ts`</span>)
})

<span class="hljs-comment">// import this function and call it with the appropriate props</span>
<span class="hljs-keyword">const</span> cognitoAuth = createTravelUserpool(<span class="hljs-built_in">this</span>, {
    appName: context.appName,
    env: context.environment,
    addUserPostConfirmation: addUserFunc,
})

<span class="hljs-comment">// ...databases we created in the previous chapter</span>
</code></pre>
<blockquote>
<p>🗒️ A note on pricing:</p>
<p>Amazon Cognito has one of the most generous pricing tiers of any fully managed auth provider. Every account is allowed 50,000 monthly active users as part of the free tier and this extends past the first 12 months of an account being created.</p>
<p>Beyond that, each user has a cost of $0.05. While this may seem pricy, my take is that if you have over 50k MAU's signed up using your app and you haven't figured out a way to monetize that, come see me. We have bigger things to talk about 😜</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679286364401/a7b40bcd-eaff-4277-a440-a794b02a2337.png" alt class="image--center mx-auto" /></p>
</blockquote>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this post, we talked about authentication and authorization as they relate to both Amazon Cognito, and our Trip Logger application. Additionally, we added our Lambda function as a <code>postConfirmation</code> trigger. This enables us to display profile information to our users in a way where it can be updated without them having to log in and out.</p>
<p>Due to Cognito being made up of child services, it's hard to explain how little fun it is to perform what we accomplished by clicking through the AWS console. Even today, it would trip me up. This is truly one of the best areas to showcase the power of the AWS CDK and infrastructure-as-code in general.</p>
<p>In the next post, we'll talk about asset storage with one of the very first services that AWS came out with Amazon S3.</p>
<p>Until then, happy Coding!</p>
<p>🦦</p>
]]></content:encoded></item><item><title><![CDATA[AWS CDK for Frontend Developers: Databases and Serverless Functions]]></title><description><![CDATA[In the previous post, we scaffolded our CDK project with context and additional props. In this post, we'll leverage constructs from the CDK to create DynamoDB tables, as well as a serverless function. In addition to creating the services themselves, ...]]></description><link>https://blog.focusotter.com/aws-cdk-for-frontend-developers-databases-and-serverless-functions</link><guid isPermaLink="true">https://blog.focusotter.com/aws-cdk-for-frontend-developers-databases-and-serverless-functions</guid><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Startups]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[full stack]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Sun, 19 Mar 2023 09:01:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679289919996/8677cf10-7a99-4daf-850a-aa59505b8a82.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous post, we scaffolded our CDK project with <code>context</code> and additional <code>props</code>. In this post, we'll leverage <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/">constructs from the CDK</a> to create DynamoDB tables, as well as a serverless function. In addition to creating the services themselves, we'll cover roles and policies as necessary.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679203048053/8ddfd4fb-4411-4b8d-b23c-f15f3c50d471.png" alt class="image--center mx-auto" /></p>
<p>Recall from our architecture diagram that we have two tables:</p>
<ul>
<li><p><strong>TravelPost</strong>: This will contain information about the trip that was taken. For our application, full CRUD operations will be handled by our API.</p>
</li>
<li><p><strong>User</strong>: This will contain information about the signed-in user.</p>
</li>
</ul>
<p>In addition, we have a serverless function that will populate the <code>User</code> table once a user successfully signs up for the first time.</p>
<p>In the next chapter, we'll complete this flow by setting up authentication.</p>
<h2 id="heading-going-nosql-with-dynamodb">Going NoSQL with DynamoDB</h2>
<p>DynamoDB is a NoSQL database service provided by AWS. Unlike traditional SQL databases that store data in tables with fixed columns and relationships between them, DynamoDB stores data as JSON objects and doesn't rely on strict relationships between tables</p>
<p>From a frontend perspective, DynamoDB can be used to store and retrieve data for web or mobile applications. Moreso, it can help you store and retrieve data for your applications with low latency and high performance, and allows for flexible and scalable data models that can adapt to changing needs.</p>
<h3 id="heading-creating-the-travel-table">Creating the Travel Table</h3>
<p>To get started creating our first table, update our <code>backend-trip-post-stack.ts</code> file to look like the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { CDKContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'./../cdk.context.d'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cdk <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib'</span>
<span class="hljs-keyword">import</span> { Construct } <span class="hljs-keyword">from</span> <span class="hljs-string">'constructs'</span>
<span class="hljs-comment">// 👇 this is new</span>
<span class="hljs-keyword">import</span> {createTravelTable} <span class="hljs-keyword">from</span> <span class="hljs-string">'./database/tables'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> BackendTripPostStack <span class="hljs-keyword">extends</span> cdk.Stack {
    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
        scope: Construct,
        id: <span class="hljs-built_in">string</span>,
        props: cdk.StackProps,
        context: CDKContext
    </span>) {
        <span class="hljs-built_in">super</span>(scope, id, props)
        <span class="hljs-comment">// 👇 this is new</span>
        <span class="hljs-keyword">const</span> travelDB = createTravelTable(<span class="hljs-built_in">this</span>, {
            appName: context.appName,
            env: context.environment,
        })
    }
}
</code></pre>
<p>Note that in addition to the <code>import</code> statement, we are calling a function called <code>createTravelTable</code>. The function takes in two arguments:</p>
<ul>
<li><p><code>this</code>: This represents the current stack we are in.</p>
</li>
<li><p><code>props</code> : An object that represents values we'd like to pass. In our case, we passing the application name from our <code>cdk.context.json</code>file, as well as the current environment.</p>
</li>
</ul>
<blockquote>
<p>🗒️ I personally, like to keep my stack files to where I'm just passing values to services and the actual creation of those services happens in the related file.</p>
<p>This is a design choice and not a set convention.</p>
</blockquote>
<p>With the function in place, let's define our first construct.</p>
<p>From the <code>lib</code> directory, create <code>/database/tables.ts</code> file and paste in the following:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { Construct } <span class="hljs-keyword">from</span> <span class="hljs-string">'constructs'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> awsDynamodb <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-dynamodb'</span>
<span class="hljs-keyword">import</span> { RemovalPolicy } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib'</span>
<span class="hljs-keyword">import</span> { envNameContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../cdk.context'</span>

<span class="hljs-keyword">type</span> BaseTableProps = {
    appName: <span class="hljs-built_in">string</span>
    env: envNameContext
}
<span class="hljs-keyword">type</span> createTravelTableProps = BaseTableProps &amp; {}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createTravelTable</span>(<span class="hljs-params">
    scope: Construct,
    props: createTravelTableProps
</span>): <span class="hljs-title">awsDynamodb</span>.<span class="hljs-title">Table</span> </span>{
    <span class="hljs-keyword">const</span> travelTable = <span class="hljs-keyword">new</span> awsDynamodb.Table(scope, <span class="hljs-string">'TravelTable'</span>, {
        tableName: <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-TravelTable`</span>,
        removalPolicy:
            props.env === <span class="hljs-string">'develop'</span> ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN,
        billingMode: awsDynamodb.BillingMode.PAY_PER_REQUEST,
        partitionKey: { name: <span class="hljs-string">'id'</span>, <span class="hljs-keyword">type</span>: awsDynamodb.AttributeType.STRING },
    })

    <span class="hljs-keyword">return</span> travelTable
}
</code></pre>
<blockquote>
<p>🗒️ Since this is our first construct, we'll spend some time going over the arguments and props in a bit more detail. Over time, we'll just focus on the meaningful parts.</p>
</blockquote>
<p>The first import is the <code>Construct</code>. The <code>Construct</code> class is a base class for all constructs in AWS CDK, including the <code>cdk.Stack</code> class. By using the <code>Construct</code> type for the <code>scope</code> parameter, we ensure that the function can accept any construct, not just a <code>cdk.Stack</code> instance.</p>
<p>Next, we import <code>awsDynamodb</code>. This is the L2 construct from the <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb-readme.html">CDK library</a>. Note the import path of <code>aws-cdk-lib/aws-dynamodb</code>. All official L2 constructs are bundled under <code>aws-cdk-lib</code>.</p>
<p>For the props, we already know we'll have another table, so we're creating a <code>BaseTableProps</code> type that both will share, while still allowing each table to define its own.</p>
<p>As far as invoking the actual construct itself, we wrapped it in a function so we could pass in our <code>appName</code> and <code>environment</code> arguments.</p>
<p>All constructs are created with the <code>new</code> command and typically expect a format of <code>new Construct(scope, id, config)</code>. In the case of this table, it has the following:</p>
<ul>
<li><p><code>scope</code>: A reference to the current stack or construct.</p>
</li>
<li><p><code>id</code>: A unique identifier for the table.</p>
</li>
<li><p><code>config</code>: An object that configures the properties of the table, including:</p>
<ul>
<li><p><code>tableName</code>: The name of the table. We combine our <code>id</code> with our props.</p>
</li>
<li><p><code>removalPolicy</code>: A <code>RemovalPolicy</code> determines whether the table is deleted or retained when the stack is deleted. In <code>develop</code> we set it to destroy the table if we destroy our stack. For other branches, it'll remove it from the stack, but keep the table itself.</p>
</li>
<li><p><code>billingMode</code>: The billing mode for the table. While we can specify the read capacity to optimize our billing and throughput, most applications will benefit from the <code>PAY_PER_REQUEST</code> model that will scale up and down automatically based on our needs.</p>
</li>
<li><p><code>partitionKey</code>: The partition key for the table specifies how data is partitioned and stored in the table. In this case, the partition key is a string <code>id</code>. This serves as a unique identifier for all of the items in our database.</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>🗒️ A note on pricing:</p>
<p>Pricing for AWS services can be confusing and I'll do my best to call them out as we go along. Some services have a free tier during the first year of an AWS account opening. Others have it forever.</p>
<p>DynamoDB offers a generous free tier of 25GB of storage and 100GB of data transfer per month. This is per account, but means for our application, not only are there no servers to manage but there is also no cost.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679207129333/12411e78-192c-48e4-968c-ed12b755ba30.png" alt class="image--center mx-auto" /></p>
</blockquote>
<h3 id="heading-creating-the-user-table">Creating the User Table</h3>
<p>As for the parts we've already familiar with, let's add those in.</p>
<p>While still in the <code>lib/backend-trip-post-stack.ts</code> file, import a <code>createUserTable</code> function and add the following underneath our <code>travelDB</code> function:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//...rest of imports</span>
<span class="hljs-keyword">import</span> { createTravelTable, createUserTable } <span class="hljs-keyword">from</span> <span class="hljs-string">'./databases/tables'</span>

<span class="hljs-comment">//...rest of code</span>
<span class="hljs-comment">//userDB</span>
<span class="hljs-keyword">const</span> userDB = createUserTable(<span class="hljs-built_in">this</span>, {
    appName: context.appName,
    env: context.environment,
    addUserFunc,
})
</code></pre>
<p>Here we're calling a <code>createUserTable</code> function and in addition to our <code>context</code> props, we're also passing in the function we just created.</p>
<p>In the <code>lib/databases/tables.ts</code> file, we'll create the <code>createUserTable</code> which will look similar to what we created earlier. To do so, paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> CreateUserTableProps = BaseTableProps &amp; {}
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createUserTable</span>(<span class="hljs-params">
    scope: Construct,
    props: CreateUserTableProps
</span>): <span class="hljs-title">awsDynamodb</span>.<span class="hljs-title">Table</span> </span>{
    <span class="hljs-keyword">const</span> userTable = <span class="hljs-keyword">new</span> awsDynamodb.Table(scope, <span class="hljs-string">'UserTable'</span>, {
        tableName: <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-UserTable`</span>,
        removalPolicy:
            props.env === <span class="hljs-string">'develop'</span> ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN,
        billingMode: awsDynamodb.BillingMode.PAY_PER_REQUEST,
        partitionKey: { name: <span class="hljs-string">'id'</span>, <span class="hljs-keyword">type</span>: awsDynamodb.AttributeType.STRING },
    })

    <span class="hljs-keyword">return</span> userTable
}
</code></pre>
<h3 id="heading-creating-our-lambda-function">Creating our Lambda Function</h3>
<h4 id="heading-what-is-a-lambda-function">What is a Lambda function</h4>
<p>Our User table will be populated by a Lambda function. A Lambda function is often referred to as a "serverless function" when not referring to just AWS, I'll say Lambda function throughout this series, but just to get us on the same page, for this section I'll say serverless function.</p>
<p>As a frontend/fullstack developer, you may have an idea of what a serverless function is--and for the most part, you're probably correct, but there are some important pieces I want to make sure we understand, specifically the following:</p>
<ol>
<li><p><strong>A serverless function executes a bit of code, and then it stops.</strong> As opposed to a server (which can be eternally running), a serverless function is <em>ephemeral</em>. It spins up its own environment and when it's done executing and requests have stopped coming in, it'll eventually delete itself.</p>
</li>
<li><p><strong>A serverless function takes in an event argument. This is contextual.</strong> Just like in JavaScript anything can call a function, the same is true for a serverless function. Because of that, the <code>event</code> object that every function receives will vary based on what is invoking it.</p>
</li>
<li><p><strong>A serverless function is not an API route</strong> <strong>but a component of one.</strong> An API route is made up of a gateway (the thing that receives the HTTP request), and the function to invoke (the serverless function). This is important because we often think functions can only be invoked as part of a network request, but again, it can be from just about anything.</p>
</li>
</ol>
<hr />
<p>To emphasize that last point about anything being able to call a Lambda function, our function will be triggered every time a user signs up for our application.</p>
<h4 id="heading-defining-the-lambda-function">Defining the Lambda function</h4>
<p>A Lambda function definition is referred to as its <em>handler.</em></p>
<p>To define a handler for our Lambda function, create a new file called <code>lib/functions/addUserPostConfirmation/main.ts</code> and paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> AWS <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-sdk'</span>
<span class="hljs-keyword">const</span> docClient = <span class="hljs-keyword">new</span> AWS.DynamoDB.DocumentClient()
<span class="hljs-comment">//typeScript types so that our "event" object is defined</span>
<span class="hljs-keyword">import</span> { PostConfirmationConfirmSignUpTriggerEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-lambda'</span>

<span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> (event: PostConfirmationConfirmSignUpTriggerEvent) =&gt; {
    <span class="hljs-keyword">const</span> date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
    <span class="hljs-keyword">const</span> isoDate = date.toISOString()

    <span class="hljs-comment">//construct the param</span>
    <span class="hljs-keyword">const</span> params = {
        TableName: process.env.USER_TABLE_NAME <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
        Item: {
            __typename: <span class="hljs-string">'User'</span>,
            id: event.request.userAttributes.sub,
            createdAt: isoDate, <span class="hljs-comment">// ex) 2023-02-16T16:07:14.189Z</span>
            updatedAt: isoDate,
            username: event.userName,
            email: event.request.userAttributes.email,
        },
    }

    <span class="hljs-comment">//try to add to the DB, otherwise throw an error</span>
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> docClient.put(params).promise()
        <span class="hljs-keyword">return</span> event
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.log(err)
        <span class="hljs-keyword">return</span> event
    }
</code></pre>
<p>The AWS SDK gives us a utility for adding data to a table via JSON with a <code>DocumentClient</code>. From there, we construct or params. These exact reason for adding these items will be revealed in out API chapter, but for now, it's enough to know this object is what is going to be added to our database.</p>
<p>You should be getting TypeScript errors relating to the <code>aws-sdk</code> and the <code>aws-lambda</code> packages. To fix those, it's important to know two things:</p>
<ol>
<li><p>The AWS SDK is automatically installed in Lambda functions running in AWS. This means when we install it, we only have to install it as a dev dependency.</p>
</li>
<li><p>Recall Lambda functions are ephemeral and have their own environment. So when we install dependencies for them, they live in their own <code>package.json</code> file.</p>
</li>
</ol>
<p>From your terminal, change into the <code>lib/functions/addUserPostConfirmation</code> directory and run the following commands and the errors should disappear:</p>
<pre><code class="lang-bash">npm init -y &amp;&amp; npm i -D aws-sdk @types/aws-lambda
</code></pre>
<p>While the function itself will be used by our authentication service, we can create the function by calling the <code>NodejsFunction</code> L2 construct.</p>
<p>In the same the <code>addUserPostConfirmation</code> directory, create a file called <code>construct.ts</code> and paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Table } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-dynamodb'</span>
<span class="hljs-keyword">import</span> { Runtime } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-lambda'</span>
<span class="hljs-keyword">import</span> { NodejsFunction } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-lambda-nodejs'</span>
<span class="hljs-keyword">import</span> { Construct } <span class="hljs-keyword">from</span> <span class="hljs-string">'constructs'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> iam <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-iam'</span>
<span class="hljs-keyword">import</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>)
<span class="hljs-keyword">import</span> { envNameContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../../cdk.context'</span>

<span class="hljs-keyword">type</span> CreateAddUserPostConfirmationProps = {
    appName: <span class="hljs-built_in">string</span>
    env: envNameContext
    userTable: Table <span class="hljs-comment">// 👈 Our function expects a DynamoDB table</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createAddUserPostConfirmation = <span class="hljs-function">(<span class="hljs-params">
    scope: Construct,
    props: CreateAddUserPostConfirmationProps
</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> addUserFunc = <span class="hljs-keyword">new</span> NodejsFunction(scope, <span class="hljs-string">'addUserFunc'</span>, {
        functionName: <span class="hljs-string">`<span class="hljs-subst">${props.appName}</span>-<span class="hljs-subst">${props.env}</span>-addUserFunc`</span>,
        runtime: Runtime.NODEJS_16_X,
        handler: <span class="hljs-string">'handler'</span>,
        entry: path.join(__dirname, <span class="hljs-string">`./main.ts`</span>),
        environment: {
<span class="hljs-comment">// pass the DynamoDB table name to the function as an env var</span>
            USER_TABLE_NAME: props.userTable.tableName,
        },
    })

<span class="hljs-comment">// Give our function permission to add an item to DynamoDB</span>
    addUserFunc.addToRolePolicy(
        <span class="hljs-keyword">new</span> iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [<span class="hljs-string">'dynamodb:PutItem'</span>],
            resources: [props.userTable.tableArn],
        })
    )
    <span class="hljs-keyword">return</span> addUserFunc
}
</code></pre>
<p>When using L2 constructs, they all follow a similar pattern. So even though this is a completely different AWS service, it can be distilled down to the same signature as our previous DynamoDB tables:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">new</span> ConstructName(scope, id, configObject)
</code></pre>
<blockquote>
<p>In AWS, services are deny-by-default. This means unless permissions are defined (either explicitly or implicitly), then they can't talk to one another.</p>
<p>In the next chapter, we'll dive deeper into this concept and create our own access policies when we talk about S3.</p>
</blockquote>
<p>It's worth noting that constructing a Lambda function with the CDK comes in two flavors:</p>
<ol>
<li><p>A <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda-readme.html">generic construct</a> that allows for code to be written in many different languages</p>
</li>
<li><p><code>A</code><a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs-readme.html"><code>NodejsFunction</code> construct</a> that only works with JavaScript and TypeScript files, but handles the bundling and transpiling with <code>esbuild</code> for us.</p>
<p> Since we wrote our function in TypeScript, let's install <code>esbuild</code> as a dev dependency so that the CDK can automatically handle the transpilation process for us.</p>
</li>
</ol>
<p>In the root of our project, run the following command:</p>
<pre><code class="lang-bash">npm i -D esbuild
</code></pre>
<p>We defined our function handler, and create the construct. All that's left to do is add the function to our stack.</p>
<p>Back in our <code>lib/backend-trip-post-stack.ts</code> file, paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> addUserFunc = createAddUserPostConfirmation(<span class="hljs-built_in">this</span>, {
    appName: context.appName,
    env: context.environment,
    userTable: userDB
})
</code></pre>
<p>Congratulations! You've just learned how to create 2 databases and a Lambda function in the CDK. Furthermore, our Lambda function has environment variables and permission to talk to a database.</p>
<blockquote>
<p>🗒️ A note on pricing:</p>
<p>Per the <a target="_blank" href="https://aws.amazon.com/lambda/pricing/">AWS pricing page for Lambda</a>, "The AWS Lambda free tier includes one million free requests per month and 400,000 GB-seconds of compute time per month"</p>
<p>While the pricing can be a bit confusing due to it being based on both requests and processing time, the gist is that this is a free service for our needs and it cost fractions of a penny for many applications.</p>
<p>Feel free to checkout the pricing examples in the docs to get a more concrete example.</p>
</blockquote>
<h2 id="heading-conclusion">Conclusion</h2>
<p>With that in place, we have now successfully provisioned two DynamoDB tables and a Lambda function 🎉</p>
<p>In this section, we dove deep into how CDK constructs are initialized, the arguments they receive and how to add TypeScript types to them. We also discussed what a Lambda function is, and how to create one in TypeScript.</p>
<p>Hopefully, at this point, you're starting to see that creating AWS services in code is part understanding concepts and part calling methods available on L2 constructs. Once those are in place, it's a matter of figuring out how best to organize your code.</p>
<p>In the next chapter, we'll talk all about authentication and authorization with Amazon Cognito.</p>
<p>Until then, happy coding!</p>
<p>🦦</p>
]]></content:encoded></item><item><title><![CDATA[AWS CDK for Frontend Developers: The Ultimate Guide to Get You Started]]></title><description><![CDATA[Intro
In tech, solutions are often ended with "...but it depends". Few things are absolute. However, there is something everyone can agree on: It's best to learn by doing.
So when I set out to create a guide for frontend developers working with the A...]]></description><link>https://blog.focusotter.com/aws-cdk-for-frontend-developers-the-ultimate-guide-to-get-you-started</link><guid isPermaLink="true">https://blog.focusotter.com/aws-cdk-for-frontend-developers-the-ultimate-guide-to-get-you-started</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[full stack]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Startups]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Sun, 19 Mar 2023 04:55:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679156038320/313068be-b268-4a85-9167-4e8f4019030e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-intro">Intro</h2>
<p>In tech, solutions are often ended with "...but it depends". Few things are absolute. However, there is something everyone can agree on: It's best to learn by doing.</p>
<p>So when I set out to create a guide for frontend developers working with the AWS CDK, I didn't want to tell concepts (and if you've been following my blog for a while, you know that's not my style). I wanted to deliver a guided path towards building a fullstack, fully-typed, fully serverless project so that frontend developers can automate their backend infrastructure.</p>
<p>I keep saying this is meant for "frontend" developers, but when it comes to modern frontend development, that definition is pretty loose. As mentioned in the initial chapter of this series, there is a specific demographic that I'm targeting.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.focusotter.cloud/unleash-your-full-stack-potential-why-frontend-developers-should-embrace-aws">https://blog.focusotter.cloud/unleash-your-full-stack-potential-why-frontend-developers-should-embrace-aws</a></div>
<p> </p>
<h2 id="heading-prereqs">Prereqs</h2>
<p>While it's possible to read certain chapters in this series to pick up a particular solution, it's meant to be followed in sequence. At this point, you should have an idea of whether this guide is for you and also have a local environment <a target="_blank" href="https://blog.focusotter.cloud/getting-started-how-to-install-the-aws-cli-and-configure-a-cdk-typescript-project">bootstrapped and set up with the AWS CDK</a>.</p>
<h2 id="heading-series-overview">Series Overview</h2>
<p>In this chapter, we'll see how the series will flow and tour what we're building. Then we'll set up our CDK project so that it's flexible to iterate on in later chapters.</p>
<p>In particular, here's the following outline for our backend:</p>
<ol>
<li><p>Understanding if this series is for you</p>
</li>
<li><p><strong>Setting up an AWS account</strong> and the AWS CDK</p>
</li>
<li><p><code>You are here</code> <strong>👉🏽</strong> Project overview and setup</p>
</li>
<li><p><strong>DynamoDB and Lambda</strong>: Creating your first CDK services</p>
</li>
<li><p><strong>Amazon Cognito</strong>: Adding authentication and authorization</p>
</li>
<li><p><strong>Simple Storage Service (S3) and Cloudfront</strong>: Adding image storage</p>
</li>
<li><p><strong>AWS AppSync</strong>: Modern API Development</p>
</li>
<li><p><strong>Deploying to AWS</strong>: CDK context and GitHub actions</p>
</li>
</ol>
<p>Also, because this is a fullstack series, we'll bring our infrastructure to life on the frontend:</p>
<ol>
<li><p><strong>Frontend setup</strong> NextJS, <a target="_blank" href="https://tailwindcss.com/">TailwindCSS</a>, and <a target="_blank" href="https://cloudinary.com/">Cloudinary</a>(!)</p>
</li>
<li><p><strong>Frontend to backend</strong> with <a target="_blank" href="https://docs.amplify.aws/">AWS Amplify libraries</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/amplify/"><strong>AWS Amplify Hosting</strong></a>: Considerations and walkthrough</p>
</li>
</ol>
<p><strong>[Bonus]</strong>: Fullstack SaaS starter with <a target="_blank" href="https://stripe.com/">Stripe</a></p>
<p>With an understanding of what services we'll be making use of, let's get started building our project!</p>
<h2 id="heading-aws-cdk-project-setup">AWS CDK Project Setup</h2>
<hr />
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679162234263/660e31d5-2117-4af0-adb5-b365c186cdd4.png" alt="home page with navbar" class="image--center mx-auto" /></p>
<hr />
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679162264102/3005177c-2129-4e2e-95c1-74d2f04420da.png" alt="homepage with items" class="image--center mx-auto" /></p>
<hr />
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679162324710/7dac324c-3863-4dd7-b5fc-3d7d73661d97.png" alt="protected page to create a trip item" class="image--center mx-auto" /></p>
<hr />
<p>The project we'll build is a trip posting app where we can share with others the trips that we've been on. While sounding simple, take a moment to think about all of the features our app will require and how they match the upcoming chapters. You'll soon realize this is a core functionality found in most modern applications that hope to be more than a public to-do site.</p>
<p>In a new directory, run the following command:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># My direcotry: ~/Projects/fullstack-cdk-series/backend-trip-post</span>
npx aws-cdk@latest init -l typescript
</code></pre>
<blockquote>
<p>🗒️ For our backend, I'll be working from a <code>backend-trip-post</code> directory.</p>
</blockquote>
<p>This will initialize a new CDK project using the latest version. Specifically, we are setting the language to TypeScript (both <code>--language</code>, or <code>-l</code> work).</p>
<p>Once installed, you should receive output similar to the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679162887893/1c2fa7e2-fcb4-4bf7-8932-cfc66b77892e.png" alt class="image--center mx-auto" /></p>
<p>Right away, you can see the CDK is here to inform us of useful commands. To expand on this, open up the project in your editor (I'll be using VS Code) and let's go over a few of the generated files:</p>
<ul>
<li><p><code>npm run build</code>: We're writing TypeScript, and this command uses <code>tsc</code> to convert it to JavaScript</p>
</li>
<li><p><code>npm run watch</code>: Some services like Lambda can be watched and redeployed upon save instead of having a full redeploy</p>
</li>
<li><p><code>npm run test</code>: <s>haha lol</s> runs our test suite</p>
</li>
<li><p><code>lib/backend-trip-post-stack.ts</code> : In JavaScript, you can call multiple functions at the same time by wrapping them in a larger function. This is where we define that larger function--called a stack.</p>
</li>
<li><p><code>bin/backend-trip-post.ts</code> : Our projects can contain multiple stacks. This is where we tell our CDK app instance(<code>new cdk.App()</code>)about them. Since AWS has multiple regions around the world and it's possible to deploy our application to various accounts, this is also where we pass that configuration to our stacks. If left blank, it will use the values from our local AWS profile.</p>
</li>
</ul>
<p>In short, what we deploy to AWS is one or many stacks. These stacks can be configured with the account, region, and additional context we provide.</p>
<p>A stack is made up of <em>constructs</em>. But what's a construct?</p>
<h3 id="heading-constructs">Constructs</h3>
<p>Recall that the AWS CDK is a wrapper around CloudFormation, and that CloudFormation is AWS's way of templating AWS services.</p>
<p>A construct is a wrapper around a CloudFormation-defined AWS service. That means instead of writing YAML, we get to use a TypeScript. These constructs can exist in one of three levels depending on how abstract you want to be.</p>
<p>While sounding (super) confusing at first, as a frontend developer, you're actually already familiar with this concept due to React's components:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679166627717/442ccdca-f39e-45f2-bce7-245c675ff895.png" alt class="image--center mx-auto" /></p>
<p>In the above screenshot, we see an example of a level 1 construct in purple. The base is HTML, and the abstraction is JSX. At level 1, even though we're in React, our code is mirroring what's available in HTML.</p>
<p>At level two, we use React as intended. When people think of React, this is what typically comes to mind. Instead of mirroring HTML, our JSX has sensible defaults and opinions based on how people are likely to use it.</p>
<p>When we get to level 3 we trade flexibility for speed. This often means we don't have as many knobs to configure but get a solution-oriented abstraction that is usually the combination of other services.</p>
<p>When a new service at AWS comes out, the CDK is typically updated with level 1 support. This is done by pulling the CloudFormation template.</p>
<p>After that, the CDK team and the community can contribute a level 2 construct. This is what most developers prefer to work with, and what we'll be using in this series.</p>
<p>In the CDK, an L3 construct represents a solution. These are community maintained and while they can save a lot of time like deploying a static site with a few commands, they are often org specific in both implementation and what options they allow.</p>
<h2 id="heading-cdk-context-how-to-avoid-pigeonholing">CDK Context: How to avoid <strong>pigeonholing</strong></h2>
<p>We haven't written much code in this post and that makes me sad. So before wrapping up, let's make our time easier in the later stages by spending some time on our setup.</p>
<p>Similar to React, the CDK doesn't have an opinion on how you set up your project. As many devs know, this is great for getting started but can pigeonhole (put us in a situation difficult to get out of) if not careful.</p>
<p>While we'll be iterating on this process when in the deployment chapter, we can lay the foundation now.</p>
<h3 id="heading-initial-context">Initial Context</h3>
<p>When we initialized our project, the CDK generated a <code>cdk.json</code> file. For the most part, this contains configurations by the CDK and feature flags so that otherwise breaking changes by the CDK team can be safely introduced. However, we can also add additional <code>context</code> fields that are specific to how we would like our app to be deployed.</p>
<p>While it's possible to add those values in this file, I prefer putting them in a separate <code>cdk.context.json</code> file. Create this file in the root of your project:</p>
<pre><code class="lang-bash">touch cdk.context.json <span class="hljs-comment"># or right-click in your editor to create the file.</span>
</code></pre>
<blockquote>
<p>🗒️ This file is special when placed at the root of our project. The CDK knows how to read the contents of this file as we'll see later on.</p>
</blockquote>
<p>In that file, place the following:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"globals"</span>: {
        <span class="hljs-attr">"appName"</span>: <span class="hljs-string">"travel-viewer-app"</span>,
        <span class="hljs-attr">"region"</span>: <span class="hljs-string">"us-east-1"</span>,
        <span class="hljs-attr">"appDescription"</span>: <span class="hljs-string">"A stack for a Travel pic viewer"</span>
    },
    <span class="hljs-attr">"environments"</span>: [
        {
            <span class="hljs-attr">"environment"</span>: <span class="hljs-string">"develop"</span>,
            <span class="hljs-attr">"branchName"</span>: <span class="hljs-string">"develop"</span>
        }
    ]
}
</code></pre>
<p>To summarize this code: Anything in <code>globals</code> will be about our project, regardless of what branch or stage we're on. Anything in <code>environments</code> will be specific to the git branch or CDK environment we are targeting.</p>
<p>The CDK initialized a git project for us and so you should be on the <code>main</code> branch currently. That's fine and we'll check out our <code>develop</code> branch later on.</p>
<p>Once we deploy, we'll want to combine our <code>globals</code> with the specific object in our <code>environments</code> array into one complete object.</p>
<p>To make sure we have TypeScript gives us inference on that, add a <code>cdk.context.d.ts</code> file and paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> CDKContext = {
    appName: <span class="hljs-built_in">string</span>
    appDescription: <span class="hljs-built_in">string</span>
    region: <span class="hljs-built_in">string</span>
    environment: envNameContext
    branchName: branchNameContext
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> envNameContext = <span class="hljs-string">'develop'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> branchNameContext = <span class="hljs-string">'develop'</span>
</code></pre>
<p>Again, the above snippet refers to the 3 globals and the two environment-specific values.</p>
<blockquote>
<p>🗒️ The CDK has type-definition files ignored by default in the <code>.gitignore</code> file. To keep that setting, but allow this file, add the following <strong>under</strong> the <code>*.d.ts</code> mention:</p>
<p><code>!cdk.context.d.ts</code></p>
</blockquote>
<h3 id="heading-bin-directory-setup"><code>bin</code> directory setup</h3>
<p>This section is crucial to setting up our application for long-term success.</p>
<p>We need a way to get the current git branch we are on. Also, as mentioned, we'll need to combine our context file into a single object. This will be in addition to any additional props our stack needs to get set up.</p>
<p>Doing this in our <code>bin/backend-trip-post.ts</code> would make it a bit messy. So we'll do this in a new file.</p>
<p>Create the following file in our <code>bin</code> directory: <code>init-stack.ts</code></p>
<p>Once created, import the CDK library and our context types:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { CDKContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'../cdk.context'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cdk <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib'</span>
</code></pre>
<h4 id="heading-getcurrentbranch-function"><code>getCurrentBranch</code> function</h4>
<pre><code class="lang-typescript"><span class="hljs-comment">// ... other imports</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> gitBranch <span class="hljs-keyword">from</span> <span class="hljs-string">'git-branch'</span>

<span class="hljs-comment">// Get the current git branch</span>
<span class="hljs-keyword">const</span> getCurrentBranch = (): <span class="hljs-function"><span class="hljs-params">string</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> currentBranch = gitBranch.sync()
    <span class="hljs-keyword">return</span> currentBranch
}
</code></pre>
<p>Getting the branch name is simple enough with the <code>git-branch</code> package. Be sure to install that package and its separately packaged TypeScript types:</p>
<pre><code class="lang-bash">npm i git-branch &amp;&amp; npm i --save-dev @types/git-branch
</code></pre>
<h4 id="heading-getenvironmentcontext-function"><code>getEnvironmentContext</code> function</h4>
<p>Next, we'll want to get our branch, and based on that create an object that matches our <code>CDKContext</code> types.</p>
<p>To accomplish that, paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Get the environment context based on the current git branch</span>
<span class="hljs-keyword">const</span> getEnvironmentContext = <span class="hljs-function">(<span class="hljs-params">app: cdk.App</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> currentBranch = getCurrentBranch()
    <span class="hljs-keyword">const</span> environments = app.node.tryGetContext(<span class="hljs-string">'environments'</span>)
    <span class="hljs-keyword">const</span> environment = environments.find(
        <span class="hljs-function">(<span class="hljs-params">env: <span class="hljs-built_in">any</span></span>) =&gt;</span> env.branchName === currentBranch
    )
    <span class="hljs-keyword">const</span> globals = app.node.tryGetContext(<span class="hljs-string">'globals'</span>)

    <span class="hljs-keyword">return</span> { ...globals, ...environment }
}
</code></pre>
<p>Nothing too fancy here--which is how I like to keep things, but what you'll notice is the <code>app.node.tryGetContext</code>. By defining our context in a <code>cdk.context.json</code> file, we can call this method anywhere in our application to retrieve values. From there, we use a combination of the <code>getCurrentBranch</code> method and the <code>find</code> method on arrays to create our object.</p>
<h4 id="heading-initstack-function"><code>initStack</code> function</h4>
<p>The last function in this file is what we'll end up calling in our <code>backend-trip-post.ts</code> file.</p>
<p>Recall that so far we've defined the custom properties that we'll provide to our stack. However, the CDK requires its own set. Specifically, it needs to know the AWS environment to publish our app to, and a name for the stack. Optionally, we can tell it things like what account, tags, and name of the stack.</p>
<p>To see this in action, paste in the following:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Initialize the stack</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> initStack = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> cdk.App()
    <span class="hljs-keyword">const</span> context = getEnvironmentContext(app) <span class="hljs-keyword">as</span> CDKContext
    <span class="hljs-keyword">const</span> stackName = <span class="hljs-string">`<span class="hljs-subst">${context.appName}</span>-stack-<span class="hljs-subst">${context.environment}</span>`</span>

    <span class="hljs-comment">// tag resources in AWS to find the easier</span>
    <span class="hljs-keyword">const</span> tags = {
        Environment: context.environment,
        AppName: <span class="hljs-string">`<span class="hljs-subst">${context.appName}</span>`</span>,
    }

    <span class="hljs-comment">// Provide required properties to our Stack</span>
    <span class="hljs-keyword">const</span> stackProps: cdk.StackProps = {
        env: {
            region: context.region,
        },
        stackName: stackName,
        description: context.appDescription,
        tags,
    }

    <span class="hljs-keyword">return</span> {
        app,
        stackNameWithEnv: stackName,
        stackProps,
        context,
    }
}
</code></pre>
<h4 id="heading-passing-values-to-our-stack">Passing values to our stack</h4>
<p>When the above <code>initStack</code> function gets called it returns everything needed for our stack. Let's complete this post by initializing our stack and updating its values to accept them.</p>
<p>In <code>bin/backend-trip-post.ts</code> replace the existing code so that it resembles the following:</p>
<pre><code class="lang-typescript"><span class="hljs-meta">#!/usr/bin/env node</span>
<span class="hljs-keyword">import</span> { initStack } <span class="hljs-keyword">from</span> <span class="hljs-string">'./init-stack'</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'source-map-support/register'</span>
<span class="hljs-keyword">import</span> { BackendTripPostStack } <span class="hljs-keyword">from</span> <span class="hljs-string">'../lib/backend-trip-post-stack'</span>

<span class="hljs-keyword">const</span> { app, stackNameWithEnv, stackProps, context } = initStack()

<span class="hljs-keyword">const</span> travelStack = <span class="hljs-keyword">new</span> BackendTripPostStack(
    app,
    stackNameWithEnv,
    stackProps,
    context
)
</code></pre>
<p>If all went well, you should see TypeScript complaining that <code>BackendTripPostStack</code> doesn't know about <code>context</code>.</p>
<p>Update that file so that the default code is replaced with an updated version that not only takes in our <code>context</code>, but no longer assumes the <code>props</code> are optional:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { CDKContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'./../cdk.context.d'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cdk <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib'</span>
<span class="hljs-keyword">import</span> { Construct } <span class="hljs-keyword">from</span> <span class="hljs-string">'constructs'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> BackendTripPostStack <span class="hljs-keyword">extends</span> cdk.Stack {
    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
        scope: Construct,
        id: <span class="hljs-built_in">string</span>,
        props: cdk.StackProps,
        context: CDKContext
    </span>) {
        <span class="hljs-built_in">super</span>(scope, id, props)
        <span class="hljs-comment">// our code will go here</span>
    }
}
</code></pre>
<p>Congratulations, you've just completed the hardest part of this entire series 🎉</p>
<blockquote>
<p>🗒️ I want to take a moment to emphasize that we didn't <em>have</em> to construct our app like this no more than on the frontend we don't have to split out our code.</p>
<p>However, some things remain true across stacks. Learning how to set up a project based on what you're trying to accomplish is one of them</p>
</blockquote>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this chapter, we talked about how to initialize a CDK project and the various files that come with it. We compared CDK wrapping CloudFormation to JSX wrapping HTML. This analogy extended to constructs being similar to React components before getting our stack set up to accept custom values.</p>
<p>In the next chapter of this series, we'll dive into how simple it can be to work with constructs by creating our databases and a Lambda function.</p>
<p>In the meantime, I'm looking forward to your comments, suggestions, and feedback!</p>
<p>Happy Coding,</p>
<p>🦦</p>
]]></content:encoded></item><item><title><![CDATA[Getting started: How to install the AWS CLI and configure a CDK TypeScript project]]></title><description><![CDATA[As a frontend developer, you're probably used to working with modern web frameworks like React or Vue, which allow you to build complex applications using familiar programming languages. The AWS Cloud Development Kit (CDK) is similar in that it lets ...]]></description><link>https://blog.focusotter.com/getting-started-how-to-install-the-aws-cli-and-configure-a-cdk-typescript-project</link><guid isPermaLink="true">https://blog.focusotter.com/getting-started-how-to-install-the-aws-cli-and-configure-a-cdk-typescript-project</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Startups]]></category><category><![CDATA[full stack]]></category><category><![CDATA[aws-cdk]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Thu, 16 Mar 2023 05:41:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/s8TQiOfGlz0/upload/d7ccf4019599058502a9148428114ed0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a frontend developer, you're probably used to working with modern web frameworks like React or Vue, which allow you to build complex applications using familiar programming languages. <a target="_blank" href="https://aws.amazon.com/cdk/">The AWS Cloud Development Kit (CDK)</a> is similar in that it lets you define your infrastructure using code, but for the cloud instead of the web.</p>
<p>With the CDK, you can use TypeScript to define your cloud infrastructure, including services like Amazon S3, AWS Lambda, and Amazon DynamoDB. By defining your infrastructure in one place, it can be versioned and easily reproduced. This is a huge benefit over clicking through the AWS console.</p>
<p>AWS has a service that already does this: CloudFormation. But it's known for being verbose. The CDK is a wrapper around CloudFormation that also comes with a set of libraries to make it easy to define best practices and security policies for your cloud resources. This means that you can ensure that your infrastructure is secure and follows AWS best practices without having to spend too much time learning about the specifics upfront.</p>
<p>In this post, we'll walk through how to get started with the CDK, starting with creating an AWS user and installing the AWS CLI. In later posts, we'll also deploy services and bring them into our frontend applications.</p>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<ul>
<li><p><a target="_blank" href="https://nodejs.org/en/">Node.js</a> version 16 or later is installed.</p>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/">NPM</a> is installed.</p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/console/">An AWS account</a></p>
</li>
<li><div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/FAfhMXUiLuU">https://youtu.be/FAfhMXUiLuU</a></div>
<p> </p>
</li>
<li><div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=UnqxiSJEZAk">https://www.youtube.com/watch?v=UnqxiSJEZAk</a></div>
<p> </p>
</li>
</ul>
<h2 id="heading-steps"><strong>Steps</strong></h2>
<h3 id="heading-optional-creating-a-new-aws-user-with-admin-role"><strong>[optional] Creating a New AWS User with Admin Role</strong></h3>
<p>First you need to create a new AWS user with the appropriate permissions. Follow these steps to create a new user with the <strong>Administrator</strong> role and generate an access key and secret access key:</p>
<ol>
<li><p>Sign in to the <a target="_blank" href="https://aws.amazon.com/console/"><strong>AWS Management Console</strong></a>.</p>
</li>
<li><p>Navigate to the <a target="_blank" href="https://console.aws.amazon.com/iam/"><strong>IAM (Identity and Access Management) service</strong></a>.</p>
</li>
<li><p>In the navigation pane, click on <strong>Users</strong> and then click the <strong>Add users</strong> button.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678943576127/f691a6d9-582e-4d42-99fe-bd8f3edc8f43.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Enter a <strong>User name</strong> and select the <strong>Programmatic access</strong> checkbox in the "Select AWS access type" section (adding console access is optional but preferred). Click <strong>Next: Permissions</strong>.</p>
</li>
<li><p>You can choose to add the user to an existing group, copy permissions from an existing user, or attach policies directly. For this guide, we will add the user to an existing group. Click the <strong>Add user to group</strong> tab.</p>
</li>
<li><p>Click the <strong>Create group</strong> button to create a new group with the Administrator role. Enter a name for the group, such as "Admins".</p>
</li>
<li><p>In the search bar, type <code>AdministratorAccess</code> and select the checkbox next to the <code>AdministratorAccess</code> policy. Click <strong>Create group</strong>.</p>
</li>
<li><p>You should now see the newly created "Admins" group. Select the checkbox next to the group and click <strong>Next: Tags</strong>.</p>
</li>
<li><p>(Optional) Add any tags you want to associate with the user, then click <strong>Next: Review</strong>.</p>
</li>
<li><p>Review the user details and permissions, then click <strong>Create user</strong>.</p>
</li>
<li><p>After the user is created, you will see the <strong>Access key ID</strong> and <strong>Secret access key</strong> for the new user. <strong>Important:</strong> This is the only time you will be able to view the secret access key, so be sure to download the .csv file or copy the keys to a safe location.</p>
</li>
</ol>
<p>Now that you have created a new AWS user with the Administrator role and obtained the access key and secret access key, you can proceed to configure the AWS CLI using the <code>aws configure</code> command. Enter the access key and secret access key when prompted.</p>
<ol>
<li><strong>Install AWS CLI</strong></li>
</ol>
<p>Follow the installation instructions from the <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"><strong>official AWS documentation</strong></a>.</p>
<ol>
<li><strong>Configure a default AWS profile</strong></li>
</ol>
<p>To configure your AWS CLI, open a terminal and run the following command:</p>
<pre><code class="lang-bash">aws configure
</code></pre>
<p>You will be prompted to enter your <code>AWS Access Key ID</code>, <code>AWS Secret Access Key</code>, <code>Default region name</code>, and <code>Default output format</code>. Fill in the details and press Enter.</p>
<ol>
<li><strong>Install AWS CDK using npx</strong></li>
</ol>
<p>Instead of installing the AWS CDK globally, we will use <code>npx</code> to run the latest version:</p>
<pre><code class="lang-bash">npx aws-cdk@latest
</code></pre>
<ol>
<li><strong>Bootstrap your environment</strong></li>
</ol>
<p>Run the following command to bootstrap your environment. This is a one-time-per-region process and essentially authorizes the CDK to deploy resources on your behalf:</p>
<pre><code class="lang-bash">npx aws-cdk bootstrap
</code></pre>
<ol>
<li><strong>Initialize a new CDK project with TypeScript</strong></li>
</ol>
<p>Create a new directory for your CDK project and navigate to it:</p>
<pre><code class="lang-bash">mkdir my-cdk-project
<span class="hljs-built_in">cd</span> my-cdk-project
</code></pre>
<p><em>Note: Initializing a new CDK project should be done in an empty directory.</em></p>
<p>Now, initialize the new CDK project with TypeScript:</p>
<pre><code class="lang-bash">npx aws-cdk init --language typescript
</code></pre>
<h2 id="heading-running-cdk-commands"><strong>Running CDK commands</strong></h2>
<p>Whenever you need to run CDK commands, use <code>npx aws-cdk</code>:</p>
<pre><code class="lang-bash">npx aws-cdk &lt;<span class="hljs-built_in">command</span>&gt;
</code></pre>
<p>For example, to list your CDK stacks:</p>
<pre><code class="lang-bash">npx aws-cdk list
</code></pre>
<p>To synthesize your CDK app:</p>
<pre><code class="lang-bash">npx aws-cdk synth
</code></pre>
<p>Now you have successfully installed AWS CDK version 2 with NPM, configured the AWS CLI, and initialized a new CDK project with TypeScript.</p>
<p>If wanting to destroy your CDK app:</p>
<pre><code class="lang-bash">npx aws-cdk destroy
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That's it! You have successfully installed AWS CDK version 2 with NPM and configured it for use with the AWS CLI.</p>
<p>In the next post, we'll create our first AWS services</p>
<p>Happy coding🦦</p>
]]></content:encoded></item><item><title><![CDATA[Unleash Your Full-Stack Potential: Why Frontend Developers Should Embrace AWS]]></title><description><![CDATA[If there's a particular community I love, it's the indie hacker / small startup / SaaS communities--the ones building beautiful frontend applications powered by a suite of backend services.
They're usually a group of 1-5 individuals with skills that ...]]></description><link>https://blog.focusotter.com/unleash-your-full-stack-potential-why-frontend-developers-should-embrace-aws</link><guid isPermaLink="true">https://blog.focusotter.com/unleash-your-full-stack-potential-why-frontend-developers-should-embrace-aws</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[full stack]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[Startups]]></category><dc:creator><![CDATA[Michael Liendo]]></dc:creator><pubDate>Tue, 14 Mar 2023 03:02:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679019489512/69c9d4e4-4932-409f-8d5f-0ae924b1052c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If there's a particular community I love, it's the indie hacker / small startup / SaaS communities--the ones building beautiful frontend applications powered by a suite of backend services.</p>
<p>They're usually a group of 1-5 individuals with skills that vary across the development stack. From what I've noticed lately, the trend seems to be that individuals are increasingly frontend developers with a knack for backend development.</p>
<p>What's interesting about this group is their tenacity. They build, get stuck, problem-solve, and listen to their customers to figure out what's next in the pipeline. If I had to make a few more generalizations it's that they favor speed, a good DX, and are not afraid of delivering something less than perfect if it still means delivering <em>something</em>.</p>
<p>This group is constantly seeking ways to optimize its workflow and create powerful, scalable applications that delight customers. While there are lots of frontend frameworks that help with this, some more than others are standing out on the frontend: <strong>React/NextJS, Stripe, TailwindCSS, and TypeScript</strong>.</p>
<p>The backend is where I see the most decision paralysis. Rightfully so: authN/authZ, file storage, database flexibility, and an API are common to every real-world app, and not always easy to migrate from when needing to. This often leads to picking what is easiest to get started with.</p>
<p>However, while many devs/founders consider the day 1 experience or the first 100 customers, it's important to think about day 100 or how things look when you have 1000 customers. When set up correctly, you sleep all the same.</p>
<p>There are lots of players and opinions in this space, but in this post, we'll discuss why embracing AWS, specifically a core selection of tools and services, can not only unlock your full-stack potential but also elevate your application development so that your customers benefit from it.</p>
<p>So, why should you build your next application on AWS? Here are some compelling reasons and services that you should consider:</p>
<h3 id="heading-slightly-more-technical-overhead-but-worth-it">Slightly More Technical Overhead, But Worth It</h3>
<p><img src="https://imgs.search.brave.com/rvbw2t1OTukWRUOguEmg_j2aykJ_tU2OfgESHl0WZXE/rs:fit:1200:1200:1/g:ce/aHR0cDovL2ltZzEu/ZXRzeXN0YXRpYy5j/b20vMDAwLzAvNTg4/MDE4NC9pbF9mdWxs/eGZ1bGwuMTc2NTc1/MTg1LmpwZw" alt="Vintage Balance Scale Scales of Justice by JoieDeCleve on Etsy" /></p>
<p>Adopting this AWS-based stack might come with slightly more technical overhead compared to your current workflow. However, the numerous benefits it offers far outweigh this factor. By diving into the AWS ecosystem, you'll gain access to a world of powerful services and tools, enabling you to build highly-scalable, cost-effective applications with ease. It's important to note that you developing on AWS doesn't mean using all of the 200+ services they offer. Instead, it's about picking the right service for your particular need.</p>
<p>The added technical overhead will be a valuable investment in your skillset, making you a more versatile developer. As you become proficient in AWS services, you'll find that the initial learning curve pays off in the long run, helping you create more sophisticated applications and positioning you for greater opportunities in the tech industry.</p>
<h3 id="heading-cost-effective-compared-to-3rd-party-low-code-tools">Cost-effective compared to 3rd-party low-code tools</h3>
<p>While low-code tools with monthly subscriptions may seem tempting due to how easy they are to get started, AWS offers a more cost-effective and flexible solution when making the most of serverless/managed services. With those, you only pay for the resources you consume, allowing you to optimize costs as your application scales.</p>
<p>That isn't to say low-code tools don't have their place or benefit. Rather, it's about retaining ownership, and thus having more control over what is happening in your application. Recall that AWS is an ecosystem of services. Having services that know how to talk to and integrate is a huge benefit. In the end, low-code tools remain extremely viable (AWS has several), and when considering a 3rd-party tool, there is of "Better-Together" workflow as well.</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/focusotter/status/1628053794691899393?s=20">https://twitter.com/focusotter/status/1628053794691899393?s=20</a></div>
<p> </p>
<h3 id="heading-bias-towards-serverless-andamp-managed-services">Bias Towards Serverless &amp; Managed Services</h3>
<p>Touched on above, but focusing on serverless and managed services offered by AWS enables you to build scalable, cost-effective applications without managing servers. I think it seems fair: If I'm not using a service, then don't charge me.</p>
<p>AWS offers a range of serverless services like AWS Lambda, Amazon API Gateway, and Amazon DynamoDB, allowing you to build and deploy your applications quickly, with built-in scalability and cost optimization.</p>
<p>As an indie creator or SaaS founder, cost and durability are likely your biggest factors. This is where a managed service comes into play. <em>Managed</em> means less time provisioning throughput, less code you have to write, and more time focusing on your product and customers.</p>
<p>When I tell folks that, they often think this means more Lambda functions. I'm saying <em>fewer</em> Lambda functions. A large benefit of serverless, fully-managed services on AWS is there are often direct integrations available so they can talk to one another. Less Lambda functions mean fewer cold starts.</p>
<p>AWS StepFunctions, EventBridge Pipes, and AWS AppSync are great examples of this.</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/focusotter/status/1614040726320418816?s=20">https://twitter.com/focusotter/status/1614040726320418816?s=20</a></div>
<p> </p>
<h3 id="heading-increased-earning-potential">Increased earning potential</h3>
<p>I've been speaking from the perspective of an indie creator or SaaS founder, and while that's intentional, I wanted to add this blurb before getting into the stack itself.</p>
<p>Maybe the product is <em>you</em> and you're consulting or offering time-based services. Learning AWS can significantly boost your earning potential. As one of the most in-demand cloud platforms, having AWS skills on your resume can open up lucrative opportunities in the job market. You don't need to have every certification, or even be some AWS guru. You can still position yourself as an invaluable full-stack developer, ready to tackle complex projects and contribute to the growth of startups and SaaS businesses.</p>
<hr />
<p>Ok, so if you've made it this far, you may be wondering what a fullstack project on AWS looks like and of the 200+ services, which ones are core for most SaaS apps and indie creators.</p>
<p>Let's discuss it!</p>
<h3 id="heading-modern-frontend-development-with-nextjs-and-tailwind">Modern Frontend Development with NextJS and Tailwind</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679004658458/632c4f73-5613-402a-b32f-c2b1e15412c5.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>With almost half of all respondents in the 2022 State of JS survey using NextJS and a 90% retention rate, NextJS has become the staple when it comes to application development.</p>
</blockquote>
<p>Embracing modern frontend development means adopting powerful tools like NextJS and Tailwind CSS. NextJS, a popular framework built on top of React, brings numerous benefits, such as built-in TypeScript support, which enables robust type-checking and code autocompletion, and file-based routing for effortless page creation and navigation. With its hybrid rendering, you choose between server-side rendering (SSR), static site generation (SSG), or client-side rendering, depending on your application's needs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679004998489/e6b724f9-9d7b-4ebe-8bf4-b617bc192c4e.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>With 46% of the State of CSS survey respondents using Tailwind, and the highest retention rate (79%) of CSS frameworks, Tailwind should be your default CSS framework when building new applications.</p>
</blockquote>
<p>Tailwind CSS is a utility-first CSS framework that streamlines the styling process, making it faster and more efficient. It promotes consistency, maintainability, and readability, allowing you to create responsive, modern designs without writing custom CSS classes. By <a target="_blank" href="https://tailwindcss.com/docs/guides/nextjs">combining NextJS and Tailwind CSS</a>, you can build high-performance, visually appealing applications while benefiting from an enhanced development experience that keeps you focused on what truly matters: delivering innovative solutions for your customers.</p>
<h3 id="heading-low-barrier-to-entry-with-aws-cdk-and-typescript">Low Barrier-to-Entry with AWS CDK and TypeScript</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679005525077/685c3714-64e9-4341-8fed-8bd221640372.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>While not showcasing every option available, the above image shows everything required to create a database and an S3 bucket that stores files.</p>
</blockquote>
<p>As a TypeScript enthusiast, you'll love working with the AWS Cloud Development Kit (CDK). Before, creating your AWS Services by clicking through the AWS Console and writing down the steps you took so you didn't forget them next time. When scripting came along, it was either writing your own Bash scripts to use the AWS CLI or writing verbose templates using AWS CloudFormation.</p>
<blockquote>
<p>If you haven't used CloudFormation before, just know defining a database like the above was with <a target="_blank" href="https://s3.us-west-2.amazonaws.com/cloudformation-templates-us-west-2/DynamoDB_Table.template">a YAML file</a> several times longer.</p>
</blockquote>
<p>With the AWS CDK, you can create, test, and deploy AWS resources a lot easier, streamlining your development process and taking advantage of the full power of AWS services since it exists as a wrapper around CloudFormation.</p>
<blockquote>
<p>The AWS CDK ranks 3rd when it comes to building out fully serverless applications, has the highest retention (84%), and has the best positive/negative split.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679007451045/bdc328c3-51de-486c-8807-bebe3df3bf8d.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-simple-frontend-backend-integration-with-aws-amplify">Simple Frontend-Backend Integration with AWS Amplify</h3>
<p>AWS Amplify is a suite of services that include a CLI, hosting platform, UI Component Library and more. In particular, the Amplify JavaScript libraries provide a seamless way to connect your frontend and backend, offering pre-built UI components, authentication, and storage solutions.</p>
<p>As an example, developers often have to figure out an easy way to pass a large file from the frontend to some backend storage service. Using the Amplify libraries, files can several GB large and can be sent as a multi-part upload, paused, resumed and canceled.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679012559797/493638af-0c6b-46db-b5a7-9183ee2186e5.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-scalable-real-time-graphql-api-with-aws-appsync">Scalable, Real-Time GraphQL API with AWS AppSync</h3>
<p>As a frontend developer, you're probably familiar with the benefits of GraphQL, offering a flexible and efficient way to fetch data.</p>
<p>AWS AppSync is a fully managed GraphQL service. You supply it with a schema, give it data sources, and tell it how to connect to the data sources. As more requests come in, the service scales appropriately. If you've ever had <a target="_blank" href="https://aws.amazon.com/solutions/case-studies/taco-bell/">Taco Bell delivered to you</a>, <a target="_blank" href="https://aws.amazon.com/blogs/media/watch-now-hbo-max-uses-canaries-on-aws-for-outside-in-validation/">watched a show on HBO Max</a>, <a target="_blank" href="https://www.youtube.com/watch?v=1qYBEcG-fV4&amp;t=1s">saw stats on a live Ferrari F1 race</a>, or <a target="_blank" href="https://twitter.com/AWSstartups/status/1040844414627864576">purchased a ticket from Ticketmaster</a>, then you've already interacted with AppSync.</p>
<p>To resolve the data, you write JavaScript (native TypeScript support coming soon). Note that this isn't in a server, or even a serverless function like Lambda, this is a mapping that tells AppSync how to fetch the data. For example, the following is how a user would grab an item from DynamoDB:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679014855591/007ba567-2808-4c6d-8bd6-5236569ed62d.png" alt class="image--center mx-auto" /></p>
<p>To emphasize, when a user on the frontend makes a request, that request goes to AppSync, which then, gets the data directly from DynamoDB. No connections to manage, to extra service to invoke. This is the power of having various resources as part of the same ecosystem of services.</p>
<p>To go in the other direction, by specifying the <code>@aws_cognito_user_pools</code> directive on your schema, AppSync will automatically inspect the JWT of the request to make sure the user is authenticated with Amazon Cognito before allowing the request.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679015109437/1f92d8a5-0399-4fbd-866d-1e12d141e610.png" alt class="image--center mx-auto" /></p>
<p>Indie creators and SaaS founders of all sizes often have changing requirements and unforeseen access patterns to account for. AppSync's flexible schema model means less stress and more innovation.</p>
<p>I've written <em>a lot</em> about AppSync on this blog and <a target="_blank" href="https://aws.amazon.com/blogs/mobile/category/mobile-services/aws-appsync/page/2/">the official AWS blog</a>, so feel free to check out that content if interested in a deeper discussion!</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>Adopting AWS for your application development brings a lot of benefits, including a more streamlined and efficient development process. By leveraging the AWS CDK for TypeScript, serverless and managed services, AWS AppSync for GraphQL, and AWS Amplify for frontend-backend integration, you can fully take advantage of your full-stack potential and build innovative, scalable applications.</p>
<p>This is the first post in a series where I'll walk you through building a full-stack application using the tools and services we discussed. I'll show you how to get started, from setting up your AWS environment to deploying your NextJS application backed by core AWS services. By the end, you'll have the confidence and experience to scaffold out your next great idea on AWS,</p>
<p>See you there!</p>
<p>- 🦦</p>
]]></content:encoded></item></channel></rss>