Front-End Web & Mobile

Extend Amplify backend with custom AWS resources using AWS CDK or CloudFormation

Today, AWS Amplify announces a new amplify add custom command to add any of the 175+ AWS services to an Amplify-created backend using the AWS Cloud Development Kit (CDK) or AWS CloudFormation. The new ability to add custom resources enables developers to add additional resources beyond Amplify’s built-in use cases with a single command.

AWS Amplify is the fastest and easiest way to build cloud-powered mobile and web apps on AWS. Amplify comprises a set of tools and services that enables frontend web and mobile developers to leverage the power of AWS services to build innovative and feature-rich applications. The AWS Amplify CLI is a command line toolchain that helps frontend developers create app backends in the cloud.

Final architecture diagram

What we’ll learn

  • Add a custom AWS resource to an Amplify project
  • Make custom AWS resources compatible with Amplify’s multi-environment workflows
  • How to access custom resources from a Lambda function

What we’ll build:

  • A dinner party shopping list app that you can share with your friends to add all the ingredients
  • Ability to receive a summary email with all required ingredients using SNS to help get ready for shopping

Screenshot showing final app and email notification

Pre-requisites:

  • Install the latest Amplify CLI; version 7 and above required.
    • Open terminal and run npm i -g @aws-amplify/cli
  • Amplify CLI is already configured
    • If you haven’t configured the Amplify CLI yet, follow this guide on our documentation page

1. Initialize your React and Amplify project

First, run create a new directory and initialize your Amplify project:

npx create-react-app amplified-shopping
cd amplified-shopping
amplify init -y

An Amplify project is your starting point for developing your app backend. After an Amplify project is initialized, you can easily add backend resources such as a GraphQL API backed by Amazon DynamoDB via amplify add api.

2. Configure your app data model to store shopping list items

Intermediary architecture diagram with AppSync and DynamoDB

Next, we need to store our shopping list data somewhere. Amplify CLI provides the “API category” to help you create a GraphQL API and its underlying resources. Run amplify add api to create your GraphQL API:

amplify add api

(For the purposes of this demo, you can accept all the defaults.)
As you go through the CLI, you’ll be asked to edit your GraphQL schema. The GraphQL schema is going to define your data model and therefore also the underlying infrastructure that Amplify will generate for you.

Use the following data model for your app’s schema.graphql file:

type ShoppingItem @model { #Creates a database for ShoppingItem 
  id: ID!
  ingredient: String
  quantity: Float
  unit: String
}

type Mutation {
  sendSummaryEmail: Boolean @function(name: "sendSummary-${env}")
}

Designating a type with the “@model” directive will result in a DynamoDB table supporting with the fields of the type. In this case a “ShoppingItem” table is created with id, ingredient, quantity, and unit. The @function directive allows you to reroute a GraphQL API request to a Lambda function to handle. We’ll use this Lambda function later to publish the message to SNS which will trigger an email notification.

3. Add an Amazon SNS topic as a custom AWS resource to your Amplify project

Intermediary architecture diagram with AppSync, DynamoDB, SNS Topic, and Email notification

Now that the backend database is set up. Let’s create an Amazon SNS topic with an email subscription. Every time the SNS topic receives a message, it’ll forward it to a pre-designated email address.

Adding an SNS topic isn’t built-in to Amplify today but you can now add custom AWS resources by running:

amplify add custom

For our app, we’ll use AWS CDK to define our custom AWS resource. You can easily define this in CloudFormation as well though. Amplify CLI will open up a new “cdk-stack.ts” file for you which will include all your custom resource definitions.

Let’s edit the “cdk-stack.ts” file to create an SNS topic and an email subscription. (Hint: Make sure to replace “<YOUR_EMAIL_ADDRESS_HERE>” in the code snippet below)

import * as cdk from '@aws-cdk/core';
import * as AmplifyHelpers from '@aws-amplify/cli-extensibility-helper';
import * as sns from '@aws-cdk/aws-sns';
import * as subs from '@aws-cdk/aws-sns-subscriptions';

export class cdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps, amplifyResourceProps?: AmplifyHelpers.AmplifyResourceProps) {
    super(scope, id, props);
    /* Do not remove - Amplify CLI automatically injects the current deployment environment in this input parameter */
    new cdk.CfnParameter(this, 'env', {
      type: 'String',
      description: 'Current Amplify CLI env name',
    });
    
    // Create the SNS topic 
    const topic = new sns.Topic(this, 'sns-topic', {
      // Reference the Amplify env to ensure multi-env workflow function correctly
      topicName: `sns-topic-${AmplifyHelpers.getProjectInfo().projectName}-${cdk.Fn.ref('env')}`
    });

    // Add an email subscription
    topic.addSubscription(new subs.EmailSubscription("<YOUR_EMAIL_ADDRESS_HERE>"));
  }
}

At this point, you can try to deploy your app backend by running:

amplify push -y

When a new email subscription is set up, the email recipient needs to confirm the subscription in order to receive subsequent messages. Make sure to check your email click “Confirm subscription”.Screenshot of email pertaining to subscription confirmation

4. Configure a Lambda function to connect everything together

Architecture diagram showing the complete backend

Now that we’ve got our Email notification setup and our database configured. All that’s left to do is configuring the Lambda function to glue everything together.

amplify add function

Make sure to choose the following selections below to create a Node.js function that has access to the GraphQL API’s mutations. Very important: use the function name “sendSummary” as specified in your GraphQL schema.

? Select which capability you want to add:
> Lambda function (serverless function)
? Provide an AWS Lambda function name:
> sendSummary
? Choose the runtime that you want to use:
> NodeJS
? Choose the function template that you want to use:
> Hello World
? Do you want to configure advanced settings?
> Yes
? Do you want to access other resources in this project from your Lambda function?
> Yes
? Select the categories you want this function to have access to.
> api
? Select the operations you want to permit on amplifiedshopping
> Mutation

At the end of the CLI flow, the function code should become available for you to edit. Before we edit the Lambda function’s logic, go into the directory and add the node-fetch dependency to make it easier to call our GraphQL API.

cd amplify/backend/function/sendSummary/src
npm install node-fetch@2

Add in the following code snippet to:

  1. Fetch all shopping items
  2. Format a email message
  3. Send the email notification via SNS
/* Amplify Params - DO NOT EDIT
    API_AMPLIFIEDSHOPPING_GRAPHQLAPIENDPOINTOUTPUT
    API_AMPLIFIEDSHOPPING_GRAPHQLAPIIDOUTPUT
    API_AMPLIFIEDSHOPPING_GRAPHQLAPIKEYOUTPUT
    ENV
    REGION
Amplify Params - DO NOT EDIT */
const fetch = require("node-fetch")
const AWS = require('aws-sdk')
const sns = new AWS.SNS()
const graphqlQuery = `query listShoppingItems {
    listShoppingItems {
        items {
            id
            ingredient
            quantity
            unit
        }
    }
}
`

exports.handler = async (event) => {
    const response = await fetch(process.env.API_AMPLIFIEDSHOPPING_GRAPHQLAPIENDPOINTOUTPUT, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "x-api-key": process.env.API_AMPLIFIEDSHOPPING_GRAPHQLAPIKEYOUTPUT
        },
        body: JSON.stringify({
            query: graphqlQuery,
            operationName: "listShoppingItems",
        })
    })

    const result = await response.json()
    const shoppingItems = result.data.listShoppingItems.items

    await sns.publish({
        // For demo purposes hard-coded, normally recommended to use environment variable
        TopicArn: "<YOUR-SNS-TOPIC-ARN-HERE>",
        Message: `Here's shopping cart summary - ${new Date().toDateString()}:\n` +
            `${shoppingItems.map(item => `${item.quantity} ${item.unit} - ${item.ingredient}`)
                .join('\n')}`
    }).promise().catch(e => console.log(e))

    return true
};

Make sure you’ve updated <YOUR-SNS-TOPIC-ARN-HERE> with your SNS topic ARN. You can find the SNS Topic ARN in the subscription confirmation email, right below: “You have chosen to subscribe to the topic:”. It should look something like this: arn:aws:sns:<your-region>:<your-aws-account-id>:sns-topic-amplifiedshopping-dev.

Next, we need to add a custom IAM policy in order for the Lambda function to have access to the Publish action on this SNS topic. In your Lambda function folder: “amplify/backend/function/sendSummary/” you’ll find a “custom-policies.json”. You can define any custom IAM policies you’d like to add in this file here. To provide access to publish a message to the SNS topic, replace the custom-policies.json file with:

[
  {
    "Action": ["sns:Publish"],
    "Resource": ["arn:aws:sns:*:*:sns-topic-amplifiedshopping-${env}"]
  }
]

Now that everything is set up, deploy your backend to the cloud:

amplify push -y

5. Build your frontend

First, you need to install all the necessary dependencies for our project. Go to your React app’s root directory and run:

npm install aws-amplify recipe-ingredient-parser-v3

This will install the Amplify library, which enables your app to connect to your AWS app backend. In addition, we’ll use an open-source language parser library in order to extract structured ingredient data out of a free form text. For example, it’ll convert “six cups of milk” to {ingredient: "milk", quantity: 6, unit: "cup"}.

Now, let’s configure Amplify in your React app so it knows how to connect to your AWS app backend. Go to your index.js file and add the following three lines of code right before ReactDOM.render(...)

import Amplify from 'aws-amplify'
import awsconfig from './aws-exports'

Amplify.configure(awsconfig)

Next, replace your App.js with the following content:

import { useCallback, useEffect, useState } from 'react';
import { API } from 'aws-amplify';
import * as queries from './graphql/queries';
import * as mutations from './graphql/mutations';
import { parse } from 'recipe-ingredient-parser-v3';


function App() {
  const [ingredient, setIngredient] = useState()
  const [shoppingItems, setShoppingItems] = useState([])
  const fetchShoppingList = useCallback(async () => {
    const response = await API.graphql({ query: queries.listShoppingItems })
    setShoppingItems(response.data.listShoppingItems.items)
  })

  useEffect(() => {
    fetchShoppingList()
  }, [])

  return (
    <div className="App">
      <input value={ingredient} onChange={e => setIngredient(e.target.value)}/>
      <button onClick={async () => {
        const parsed = parse(ingredient, 'eng')
        console.log(parsed)
        if (parsed.unit && parsed.ingredient && parsed.quantity) {
          setIngredient("")
          await API.graphql({
            query: mutations.createShoppingItem,
            variables: {
              input: {
                ingredient: parsed.ingredient,
                quantity: parsed.quantity,
                unit: parsed.unit
              }
            }
          })
          fetchShoppingList()
        }
      }}>Add ingredient</button>
      <h1>Ingredients</h1>
      <button onClick={fetchShoppingList}>Refresh</button>
      <button onClick={async () => {
        await API.graphql({
          query: mutations.sendSummaryEmail
        })
      }}>Send summary email</button>
      <ul>
        {shoppingItems.map(({ ingredient, quantity, unit }) => <li>
          <div>{quantity} {unit} - {ingredient}</div>
        </li>)}
      </ul>
    </div>
  );
}

export default App;

Now test your app by running:

yarn start

Try adding a few ingredient items for your dinner party! Then hit “send summary email” and check your inbox!

GIF showing the experienceScreenshot of summary email

🥳 Success!

In this blog post you’ve learned to build a fullstack app using AWS Amplify’s API and function categories. You’ve also learned how to add custom AWS resources that aren’t built-in to Amplify yet.

What would it take to get this app to production? First and most importantly, you’d need to configure authorization rules for the GraphQL schema. In addition, you should also use IAM to authenticate the Lambda function against the GraphQL API. Last but not least, you should also introduce a mechanism to add more email recipients.

Review our documentation for more details:

Feel free to share your feedback with us GitHub or join our community on Discord.