Tuesday, February 20, 2024

AWS - Build A Serverless Web App


 ‘Run your application without servers’. The idea presented by the cloud service providers is fascinating. Of course, an application runs on virtual servers; however, the servers are not in cloud users’ hands, even invisible to the users, they are automatically allocated and managed by the cloud services. So, the developers can focus more on writing programs, and are released from hardware procurement, installation, and daily administrative tasks. As a result, it can significantly shorten time-to-market in terms of promoting an app to production. Given the nature of its loosely integrated architecture, it can improve the agility of development by a team as well. As a cloud app, it naturally bears all the cloud-specific features such as operational excellence, security, reliability, performance efficiency, cost optimization, and sustainability. 

In this blog, we are going to walk through the process of building a serverless web app using AWS and implement a few simple functions. The technologies used in the app are as follows.



AWS Amplify hosts the web site and presents the GUIs to users. A user signs up. The user’s account is created in Cognito. After signed in with the account, the user can browse the tour products and add products to the cart. The user can view what is in the cart as well. The data related to the products and the cart is stored in DynamoDB. Operations on data are executed through a REST API which calls back-end Python Lambda functions.

The source code of the app is available at GitHub.



Process of building a serverless web app


Prerequisites


We’ll use create-react-app to create the front-end app so that make sure you have Node.js and React installed.

Python is another package needed for the app. Please check out Amplify Docs to get the supported version installed. At the same time, make sure you can run pipenv from the terminal or Powershell. To do this, install pipenv and add its folder to the environment variable ‘path’.

As for Amplify, install Amplify CLI and Amplify library and Amplify UI for React.

Before we can use Amplify, we’ll need to configure it first by using ‘amplify configure’ command. Following the instructions, you’ll get there by doing it step by step. You’ll be asked to provide an IAM user during the configuration. Open IAM Console and create an IAM user with AdministratorAcess-Amplify permission granted. Below is the user created for this demo.



Additionally, React Router is used for client-side routing, and Chakra-ui is required for Toast component. 

To sum up, here is the short list of installations.

- Node.js
- React
- Python and pipenv
- Amplify (CLI, Library, and UI for React)
- react-router-dom
- Chakra-ui


Create frontend app


Run 'create-react-app' to create the front-end app. We name it as ‘demoserverlesswebapp’.
> create-react-app demoserverlesswebapp



Initialize a new backend


Move to the root folder of the app, run ‘amplify init’ command, and provide the information as necessary. 

demoserverlesswebapp> amplify init


After the initialization is done, you can see the app on Amplify Console. 



A subfolder ‘amplify’ is added to the local project, as the screenshot shows below. 




Add Authentication


Run ‘amplify add auth’, choose ‘Default configuration’.

demoserverlesswebapp> amplify add auth


Choose ‘Username’ for sign in. 



For advanced settings, you can skip it if you don’t have a need. Here, I choose ‘Yes’ because I’d like to have preferred username appearing on the signup page and appearing on the menu button after the user sign in.



Leave the following capabilities unchecked. 



The auth resource ‘demoserverlesswebapp1e89ccb3’ is added locally. 



Run ‘amplify push’ to provision it in the cloud.

demoserverlesswebapp> amplify push


As a result, we have a user pool and an identity pool created in Cognito.




Use the Authenticator component to wrap app as shown by the following image. To make it simple, only a hello message and a sign-out button are displayed on the main page at this point.



Run the app on the local host using ‘npm start’ command. 

demoserverlesswebapp> npm start

The sign in page pops up. Amazing!



Click ‘Create Account’ tab, fill out the form, and click ‘Create Account’ button.



You’ll receive an email with a confirmation code. Enter the code, click ‘Confirm’ button.



The account is created in Cognito.




Add Storage


Run ‘amplify add storage’ command.

demoserverlesswebapp> amplify add storage


Choose ‘Content’. 

For the storage name, enter ‘swastorage’.

For the bucket name, enter 'swastoragebucket’.



Choose ‘Auth and guest users’. 



Select all privileges for authenticated users.



Select ‘read’ for guest users. 

Skip adding a Lambda trigger. 



Run ‘amplify push’ to provision it in the cloud.

demoserverlesswebapp> amplify push


A new bucket has been created, with several random digits together with the environment identifier appended to the bucket name entered in the previous step. 



We create a folder named ’public’ where we store an image ‘product.jpg’ that is shared across users.




Add REST API


Run ‘amplify add api’ command, choose REST.

demoserverlesswebapp> amplify add api


For the name, enter DemoSWAApi.

For the path, enter '/product/items'.

For Lambda function name, enter ‘DemoProductLambda’, which processes requests for products.

For the runtime, choose ‘Python’.



To see what we have for advanced settings, choose ‘yes’.

We chose 'No' for all these settings for this demo.


Choose ‘yes’ for ‘Restrict API access’. 

Select all privileges for authenticated users, ‘read’ for guest users.



Let’s add another path for this app. 

Path: /cart/items
Lambda Function: DemoCartLambda, which processes requests for user cart.
Runtime: Python
API access: the same as the previous one




Run ‘amplify push’ to provision it in the cloud.

demoserverlesswebapp> amplify push


When the provision is done, the API endpoint becomes available. 

https://g0lz71cmd9.execute-api.us-west-2.amazonaws.com/dev

We can check out more details on API Gateway Console. 



Resources


Stages

The Lambda functions come into view on Lambda Console as well. But the source code is not available for edit on the Console, you’ll need to edit it locally.



To consume the REST API in program, we can call Amplify’s APIs and set the required arguments, as the following example shows. It is straightforward.

import {
  get,
  post,
} from 'aws-amplify/api';

    const restOperation = get({
      apiName: 'DemoSWAApi',
      path: '/product/items',
      options: {
        queryParams: {
          table: "Product"
        }
      }
    })
    const { body }= await restOperation.response;
    const products = await body.json();
    console.log('GetProduct call succeeded: ', products);


Deploy


Run ‘amplify add hosting’ command. 

demoserverlesswebapp> amplify add hosting

For the plugin module, choose ‘Hosting with Amplify Console’.

For the type, choose ‘Manual deployment'.



Run ‘amplify publish’ to build and publish the app to Amplify. 

demoserverlesswebapp> amplify publish


The app’s URL is generated and provided as below.



Copy and paste the URL in a web browser. As you can see, the sign in page shows up.



Open Amplify Console, click ‘demoserverlesswebapp’ under ‘All apps’ on the left pane, we can find the deployment information in ‘Hosting environments’ tab. 





Add a few business functions


To enrich the app, let’s add a few business functions.

- A user can view all available products.
- A user can select and add a product to one’s cart.
- A user can view the purchased products in one’s cart and cannot view other users’ items.
- Data of the products and users’ cart must be persistent.


As for GUI, we build a product page to list all products, a cart page to list all purchased items. 

As for navigation, we create a dropdown menu that is accessible through the top right button. You click the button; choose the page that you’d like to bring forth. 



Product page

Cart page

Data is stored in DynamoDB. Product table holds all products whereas UserCart table keeps the cart items.

The frontend app calls REST API deployed on API Gateway to fetch or update data in the database. 

The below are the definitions of the tables in DynamoDB. As a Non-SQL database, DynamoDB doesn’t support table join.


table name: Product
partition key: productid, String,
name: String,
badges: String Set
desctext: String,
imgurl: String (image file name)

table name: UserCart
partitionkey: cartid, String
sort key: productid, String
quantity: Number


The project is organized as to components' functions, illustrated by the screenshot below.

Pages go to ‘custom-ui-component’ folder.

Header menu and React Router go to ‘navigation’ folder.

REST API call part goes to ‘service’ folder.



For Amplify part, it organizes the sources by its own rules. In principle, leave the configuration files untouched unless you have mastered how all these configurations work.

For a Lambda function, we can edit the source file ‘index.py’ under its ‘src’ folder and add the required logics.. The details are findable in the app’s source code at GitHub.



To enable a Lambda function to access DynamoDB, we need to grant sufficient privilege to its execution role.

For example, choose 'DemoCartLambda-dev', the Lambda function for the cart.

Go to ‘Edit basic settings’ page.

Open IAM Console by clicking the URL under the execution role.

Add DynamoDB access permission to the role.

We’ll need to do the same to the other Lambda as well.



Lambda function’s execution role


Add AmazonDynamoDBFullAccess permission to the role






Tuesday, December 19, 2023

AWS - Build REST APIs on API Gateway and Lambda and DynamoDB

 

We break down business functions into small services, provide them through endpoints, so that they are consumed by other applications across the enterprise or from third parties depending on the use cases. Microservices advocate to loosely integrate applications with high fault tolerance as well as great agility of development. With cloud technologies, AWS extends to serverless implementation that helps customers focus on the business logic part without managing servers. Events, a change in state or an update, are produced, ingested, and routed among the serverless services. This is the core concept of Event-Driven Architecture. 

In this post, we are going to build REST APIs using a part of those cloud services. The business logic is implemented with Lambda, the data is stored in DynamoDB, and the APIs are deployed on API Gateway.



Regional Endpoint Type

Note: "Traffic that is in an Availability Zone, or between Availability Zones in all Regions, routes over the AWS private global network."

We are about to create a REST API to create a table in DynamoDB.

Table:                 Music
Partition Key:    Artist
Sort Key:           SongTitle
Attribute:           AlbumTitle

Let’s start with creating the backend Lambda function.



Create a Backend Lambda Function

 

Sign in the Management Console, open Lambda Console, click Functions on the left navigation pane. A list of functions in the Region associated with your account will be presented on the page.

 Click Create Function.



Choose Author from scratch.

Under Basic information, enter DemoCreateTable for Function name, choose Python 3.12 for Runtime



For Permissions, check Use an existing role, select TestRoleLambda for Existing role.

TestRoleLambda is a role created with AmazonDynamoDBFullAccess policy attached.

Click Create function.



The function is created, and we can view its details.



Click Code tab. You’ll see the default function which returns a message, “Hello from Lambda!”.



Replace the default function with the following source codes.

Lambda: DemoCreateTable

import json
import boto3 as bo
import botocore as bc

def lambda_handler(event, context):

    if event['headers'] is not None:
        dictparam = event['headers']
    elif event['queryStringParameters'] is not None:
        dictparam = event['queryStringParameters']
    elif event['body'] is not None:
        dictparam = json.loads(event['body'])
    else:
        return {
            'statusCode': 400, 
            'body': json.dumps('Name of the table to be created is not specified.')
        }

    try:
        tablename = dictparam['table']
        client = bo.client('dynamodb')
    
        response = client.create_table(
            AttributeDefinitions=[
                {
                    'AttributeName': 'Artist',
                    'AttributeType': 'S',
                },
                {
                    'AttributeName': 'SongTitle',
                    'AttributeType': 'S',
                },
            ],
            KeySchema=[
                {
                    'AttributeName': 'Artist',
                    'KeyType': 'HASH',
                },
                {
                    'AttributeName': 'SongTitle',
                    'KeyType': 'RANGE',
                },
            ],
            ProvisionedThroughput={
                'ReadCapacityUnits': 5,
                'WriteCapacityUnits': 5,
            },
            TableName= tablename,
        )
        
        code = 200
        msg = 'Table created'
    except bc.exceptions.ClientError as e:
        code = 500
        msg = str(e)
    except KeyError as e:
        code = 400
        msg = 'KeyError exception happened while using key {} to get the table name.'.format(str(e))

    return { 
        'statusCode': code, 
        'body': json.dumps(msg)
    }



Click Deploy




How to Test the Lambda Function?


Go to Test tab, choose Create new event, enter DemoEventCreateTable for Event name, enter {“headers”:{“table”:”Music”}} for Event JSON, and click Save



Go back to Code tab, click Test. You can check the execution results. The response says the table has been created.



After we created the Lambda function, it is time to create the REST API now.

Navigate to the API Gateway Console, click APIs on the left pane. You’ll see a list of currently available APIs.

Click Create API.



Choose REST API.



Choose New API.

Enter DemoRESTAPI for API name.

Choose Regional for API endpoint type.

Click Create API.



The Console navigates to the Resources page.

Click Create resource.



Enter DemoResourceMusic for Resource name.

Click Create resource.



DemoResourceMusic was created immediately under root “/”. We are going to create one more resource under DemoResourceMusic.

Click Create resource again.



Enter DemoResourceMusicTable for Resource name.

Click Create resource.



You can find the newly created resource, DemoResouceMusicTable, appearing in the Resources tree. We are going to create a PUT method for it.

Click Create method.



Choose PUT for Method type.

Choose Lambda function for Integration type.

Check Lambda proxy integration.



For Lambda function, choose DemoCreateTable created in the previous step.

Click Create method.



A success message pops up on the top of the page.




Test the API


We can run a test right now. How? Go to Test tab, enter “table:Music” in the Headers box, and click Test on the bottom of the page.



On the same page, we can view the results as shown on the screenshot below. We got the message, “Table created”, generated by the backend Lambda function. 



To make the API available to the consumers, we’ll need to deploy it first.

Click Deploy API.



Choose New stage for Stage.

Enter Test for Stage name.

Click Deploy.



So, the deployment was created and active for test now.



We are going to dump logs to CloudWatch to trace the API’s execution.

Click Logs and tracing.



Choose Full request and response logs for CloudWatch logs.

Enable Custom access logging.

Create a Log group named by DemoRESTAPI in the CloudWatch Console and paste its ARN in the field of Access log destination ARN.

For Log format, get the JSON template from “Learn more” link.

Click Save changes.



We also need to set up an IAM role for outputting logs to CloudWatch.

Navigate to Settings of the API, go to Logging section, and click Edit.

Choose TestRoleApiGateway, a predefined role assigned with policy AmazonAPIGatewayPushToCloudWatchLogs.



After all these are done, we can test the REST API via CloudShell using curl command.

curl -X PUT https://ixun012ycl.execute-api.us-west-2.amazonaws.com/Test/DemoResourceMusic/DemoResourceMusicTable -H 'content-type: application/json' -H 'table:Music'

We got the table created message. It works well.




Logs


Let’s check the logs and get an insight into what actually happened inside the API.

Open CloudWatch Console, click Log groups on the left pane, and we can identify the following log groups associated with the REST API.

- API-Gateway-Exection-Logs_ixun012ycl/Test

- DemoRESTAPI



The below are examples of them, respectively.



Execution Log: API-Gateway-Exection-Logs_ixun012ycl/Test


Access Log: DemoRESTAPI


What Is Passed to the Backend Lambda? 


This varies depending on enabling or disabling Lambda proxy integration setting.

In case of enabling Lambda proxy integration, the Request with the full-scale contents is handed to the backend function, as shown below.

{
    'resource': '/DemoResourceMusic/DemoResourceMusicTable',
    'path': '/DemoResourceMusic/DemoResourceMusicTable',
    'httpMethod': 'PUT',
    'headers': {'table': 'Music'},
    'multiValueHeaders': {'table': ['Music']},
    'queryStringParameters': None,
    'multiValueQueryStringParameters': None,
    'pathParameters': None,
    'stageVariables': None,
    'requestContext': {
        'resourceId': 'fby8li',
        'resourcePath': '/DemoResourceMusic/DemoResourceMusicTable',
        'httpMethod': 'PUT',
        'extendedRequestId': 'QBkk2FGJvHcFt3g=',
        'requestTime': '16/Dec/2023:07:13:34 +0000',
        'path': '/DemoResourceMusic/DemoResourceMusicTable',
        'accountId': 'nnnnnnnnnnnn',
        'protocol': 'HTTP/1.1',
        'stage': 'test-invoke-stage',
        'domainPrefix': 'testPrefix',
        'requestTimeEpoch': 1702710814759,
        'requestId': 'ab5bfc95-cf93-4eac-8357-e4a1f75f8585',
        'identity': {
            'cognitoIdentityPoolId': None,
            'cognitoIdentityId': None,
            'apiKey': 'test-invoke-api-key',
            'principalOrgId': None,
            'cognitoAuthenticationType': None,
            'userArn': 'arn:aws:iam::nnnnnnnnnnnn:user/TestIAMUser',
            'apiKeyId': 'test-invoke-api-key-id',
            'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'accountId': 'nnnnnnnnnnnn',
            'caller': 'AIDAQ3G4CFOKKZWQESTX4',
            'sourceIp': 'test-invoke-source-ip',
            'accessKey': 'ASIAQ3G4CFOKGRULRTC2',
            'cognitoAuthenticationProvider': None,
            'user': 'AIDAQ3G4CFOKKZWQESTX4'
        },
        'domainName': 'testPrefix.testDomainName',
        'apiId': 'ixun012ycl'
    },
    'body': None,
    'isBase64Encoded': False
}
 

On the contrary, only the body of the Request is passed to the backend function if Lambda proxy integration setting is disabled.

Of course, the Responses are also slightly different. The Lambda function returns status code and body.

return {
    'statusCode': code,
    'body': json.dumps(msg)
}
 

With Lambda proxy integration, the code is set as the Response’s Status Code and the message is set as its body.


Status
200
Response body
"Table created"
Response headers
{
  "X-Amzn-Trace-Id": "Root=1-657d49e2-f5f6fd248da8e0881dd97df6;Sampled=0;lineage=cdebf0f4:0"
}


Without Lambda proxy integration, the code and the message are combined and set as the Response’s body.


Status
200
Response body
{"statusCode": 200, "body": "\"Table created\""}
Response headers
{
  "Content-Type": "application/json",
  "X-Amzn-Trace-Id": "Root=1-65801497-1347f5f3c51608798c637128;Sampled=0;lineage=cdebf0f4:0"
}


 

Access Control


To manage access to a REST API, API Gateway supports several mechanisms in place, please refer to Amazon API Gateway Developer Guide for more information.

For Resource policy, you can define it in the API Gateway Console. A Resource policy is stated in the IAM policy language, here is a standard template of granting access to a list of source VPCs.

 
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "execute-api:/{{stageNameOrWildcard}}/{{httpVerbOrWildcard}}/{{resourcePathOrWildcard}}",
            "Condition": {
                "StringNotEquals": {
                    "aws:sourceVpc": "{{vpcID}}"
                }
            }
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "execute-api:/{{stageNameOrWildcard}}/{{httpVerbOrWildcard}}/{{resourcePathOrWildcard}}"
        }
    ]
}

 

More APIs


Following the procedures stated above, we can build more methods, more APIs. For example, we create another method, DELETE, for the resource DemoResourceMusicTable.



 Lambda: DemoDeleteTable

import json
import boto3 as bo
import botocore as bc

def lambda_handler(event, context):

    if event['headers'] is not None:
        dictparam = event['headers']
    elif event['queryStringParameters'] is not None:
        dictparam = event['queryStringParameters']
    elif event['body'] is not None:
        dictparam = json.loads(event['body'])
    else:
        return {
            'statusCode': 400, 
            'body': json.dumps('Name of the table to be deleted is not specified.')
        }

    try:
        tablename = dictparam['table']
        client = bo.client('dynamodb')
        
        response = client.delete_table(
            TableName = tablename,
        )
                
        code = 200
        msg = 'Table deleted'
    except bc.exceptions.ClientError as e:
        code = 500
        msg = str(e)
    except KeyError as e:
        code = 400
        msg = 'KeyError exception happened while using key {} to get the table name.'.format(str(e))
        
    return { 
        'statusCode': code, 
        'body': json.dumps(msg)
    }    
  

For the same resource, we don’t have to create a separate Lambda function for each HTTP method, indeed, we can create one Lambda function, and use httpMethod to separate processes for each method.

For DemoResourceMusicItem resource, a consolidated function is prepared for the methods.



Lambda: DemoHandleItem

import json
import boto3 as bo
import botocore as bc

def lambda_handler(event, context):

    if event['headers'] is not None:
        dictparam = event['headers']
    elif event['queryStringParameters'] is not None:
        dictparam = event['queryStringParameters']
    elif event['body'] is not None:
        dictparam = json.loads(event['body'])
    else:
        return {
            'statusCode': 400, 
            'body': json.dumps('Item to be processed is not specified.')
        }

    #
    # Add an item
    if event['httpMethod'] == 'PUT':
        
        try:
            tablename = dictparam['table']
            artist = dictparam['artist']
            songtitle = dictparam['songtitle']
            albumtitle = dictparam['albumtitle']
            
            client = bo.client('dynamodb')
            response = client.put_item(
                Item={
                    'Artist': {
                        'S': artist,
                    },
                    'AlbumTitle': {
                        'S': albumtitle,
                    },
                    'SongTitle': {
                        'S': songtitle,
                    },
                },
                ReturnConsumedCapacity='TOTAL',
                TableName = tablename,
            )
            
            code = 200
            msg = 'Item added'
        except bc.exceptions.ClientError as e:
            code = 500
            msg = str(e)
        except KeyError as e:
            code = 400
            msg = 'KeyError exception happened while using key {} to get the value.'.format(str(e))
    
        return {
            'statusCode': code,
            'body': json.dumps(msg)
        }
    #
    # Delete an item
    elif event['httpMethod'] == 'DELETE':
        try:
            tablename = dictparam['table']
            artist = dictparam['artist']
            songtitle = dictparam['songtitle']
            
            client = bo.client('dynamodb')
            response = client.delete_item(
                Key={
                    'Artist': {
                        'S': artist,
                    },
                    'SongTitle': {
                        'S': songtitle,
                    },
                },
                TableName = tablename,
            )
                    
            code = 200
            msg = 'Item deleted'
    
        except bc.exceptions.ClientError as e:
            code = 500
            msg = str(e)
        except KeyError as e:
            code = 400
            msg = 'KeyError exception happened while using key {} to get the value.'.format(str(e))
    
        return {
            'statusCode': code,
            'body': json.dumps(msg)
        }
    #
    # Select an item
    elif event['httpMethod'] == 'GET':
        try:
            tablename = dictparam['table']
            artist = dictparam['artist']
            songtitle = dictparam['songtitle']
            
            client = bo.client('dynamodb')
            response = client.get_item(
                Key={
                    'Artist': {
                        'S': artist,
                    },
                    'SongTitle': {
                        'S': songtitle,
                    },
                },
                TableName = tablename,
            )
                    
            code = 200
            if 'Item' in response.keys():
                msg = response['Item']
            else:
                msg = 'Item not found'
                
        except bc.exceptions.ClientError as e:
            code = 500
            msg = str(e)
        except KeyError as e:
            code = 400
            msg = 'KeyError exception happened while using key {} to get the value.'.format(str(e))
    
        return {
            'statusCode': code,
            'body': json.dumps(msg)
        }
    #
    # Update an item
    elif event['httpMethod'] == 'POST':
        try:
            tablename = dictparam['table']
            artist = dictparam['artist']
            songtitle = dictparam['songtitle']
            albumtitle = dictparam['albumtitle']
            
            client = bo.client('dynamodb')
            response = client.update_item(
                ExpressionAttributeNames={
                    '#AT': 'AlbumTitle',
                },
                ExpressionAttributeValues={
                    ':t': {
                        'S': albumtitle,
                    },
                },
                Key={
                    'Artist': {
                        'S': artist,
                    },
                    'SongTitle': {
                        'S': songtitle,
                    },
                },
                ReturnValues = 'ALL_NEW',
                TableName = tablename,
                UpdateExpression='SET #AT = :t',
            )
                    
            code = 200
            msg = 'Item updated'
    
        except bc.exceptions.ClientError as e:
            code = 500
            msg = str(e)
        except KeyError as e:
            code = 400
            msg = 'KeyError exception happened while using key {} to get the value.'.format(str(e))
    
        return {
            'statusCode': code,
            'body': json.dumps(msg)
        }
    #
    # Undefined request
    else:
        return {
            'statusCode': 400, 
            'body': json.dumps('Undefined request.')
        }

Let’s run a test using the following commands. 



Delete a table:

curl -X DELETE https://ixun012ycl.execute-api.us-west-2.amazonaws.com/Test/DemoResourceMusic/DemoResourceMusicTable -H 'content-type: application/json' -H 'table:Music'

Put an item:

curl -X PUT https://ixun012ycl.execute-api.us-west-2.amazonaws.com/Test/DemoResourceMusic/DemoResourceMusicItem -H 'content-type: application/json' -H 'table:Music' -H 'artist:No One You Know' -H 'albumtitle:Somewhat Famous' -H 'songtitle:Call Me Today'

Update an item:

curl -X POST https://ixun012ycl.execute-api.us-west-2.amazonaws.com/Test/DemoResourceMusic/DemoResourceMusicItem -H 'content-type: application/json' -H 'table:Music' -H 'artist:No One You Know' -H 'albumtitle: Louder Than Ever' -H 'songtitle:Call Me Today'

Get an item:

curl -X GET https://ixun012ycl.execute-api.us-west-2.amazonaws.com/Test/DemoResourceMusic/DemoResourceMusicItem -H 'content-type: application/json' -H 'table:Music' -H 'artist:No One You Know' -H 'songtitle:Call Me Today'

Delete an item:

curl -X DELETE https://ixun012ycl.execute-api.us-west-2.amazonaws.com/Test/DemoResourceMusic/DemoResourceMusicItem -H 'content-type: application/json' -H 'table:Music' -H 'artist:No One You Know' -H 'songtitle:Call Me Today'

 

AWS - Build A Serverless Web App

 ‘Run your application without servers’. The idea presented by the cloud service providers is fascinating. Of course, an application runs on...