DEV Community

Cover image for REST API using AWS SAM + AMPLIFY - Part 4
Lloyd Marcelino
Lloyd Marcelino

Posted on • Edited on

REST API using AWS SAM + AMPLIFY - Part 4

Back-end (Building SAM)

Now it is time to create our backend using AWS SAM

Step 1. Begin by generating a new directory and name it "amplify-sam-backend".

mkdir amplify-sam-backend
Enter fullscreen mode Exit fullscreen mode

Step 2. Move into the newly created directory and launch it in Visual Studio Code.

cd amplify-sam-backend
code .
Enter fullscreen mode Exit fullscreen mode

Template.yml

Subsequently, create a file named template.yml within the amplify-sam-backend directory for configuration.

Step 3. Now we will define cloud formation notations. Since AWS SAM does not have syntax, this section mainly involves copy and pasting.

  • The first step is to specify the AWS template format version, while also indicating that it incorporates the Serverless Transform.

  • Following that, we will introduce parameters to allow for dynamic code deployment to GitHub.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"

Parameters:
  GithubRepository:
    Type: String
    Description: GitHub repository URL
  Stage:
    Type: String
    Description: Deployment stage
Enter fullscreen mode Exit fullscreen mode

Step 4. We will configure global variables, specifically for our DynamoDB database, and designate the table with the name "sam-table" (this will be the name of our DynamoDB table).


Globals:
  Function:
    Environment:
      Variables:
        TABLE_NAME: sam-table

Enter fullscreen mode Exit fullscreen mode

Step 5. Next, we will focus on creating our resources, starting with the Amplify Application. We will set the front-end repository name in the property field.

NOTE: The name of our front end repository is "amplify-sam-app".

NOTE: If your repository is set to private mode, the following line should be included under the Repository field:

AccessToken: "{{resolve:secretsmanager:github-token}}"
Enter fullscreen mode Exit fullscreen mode
Name: amplify-sam-app
Repository: !Ref GithubRepository
AccessToken: "{{resolve:secretsmanager:github-token}}"
IAMServiceRole: !GetAtt AmplifyRole.Arn

Enter fullscreen mode Exit fullscreen mode

For the scope of our project, the repository is publicly accessible; therefore, including an access token is not required.

Resources:
  AmplifyApp:
    Type: AWS::Amplify::App
    Properties:
      Name: amplify-sam-app
      Repository: !Ref GithubRepository
      IAMServiceRole: !GetAtt AmplifyRole.Arn
      EnvironmentVariables:
        - Name: ENDPOINT
          Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/"
Enter fullscreen mode Exit fullscreen mode

Step 6. we will set up our Amplify branches. In our example, the main branch of our frontend application (amplify-sam-app) is labeled as "main." This serves to direct SAM on how to connect our backend with the respective branch.

NOTE: (Optional) Multiple branches can be configured if desired.

AmplifyBranch:
    Type: AWS::Amplify::Branch
    Properties:
      BranchName: main
      AppId: !GetAtt AmplifyApp.AppId
      EnableAutoBuild: true
Enter fullscreen mode Exit fullscreen mode

Step 7. Next, we will establish the necessary IAM roles and permissions required for the effective operation of Amplify. This template encapsulates all the roles and permissions that Amplify necessitates for its operation.

AmplifyRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - amplify.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: Amplify
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: "amplify:*"
                Resource: "*"
Enter fullscreen mode Exit fullscreen mode

Step 8. Next, we will build our API Gateway with CORS configuration. For this project we will set our API Gateway to public access.

NOTE: For adherence to security best practices, it is recommended to configure authorizers on your API Gateway, or alternatively, restrict access exclusively to a Virtual Private Cloud (VPC).

 MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Stage
      Cors:
        AllowMethods: "'*'"
        AllowHeaders: "'*'"
        AllowOrigin: "'*'"

Enter fullscreen mode Exit fullscreen mode

Step 9. Now lets build our resources, API Gateway methods and Lambda functions.

 MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: handler.handler
      Runtime: nodejs16.x
      Policies: AmazonDynamoDBFullAccess
      Events:
        PostApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /car
            Method: POST
        PutApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /car
            Method: PUT
        DeleteApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /car
            Method: DELETE
        HealthCheckApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /check
            Method: GET
        InventoryApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /inventory
            Method: GET


Enter fullscreen mode Exit fullscreen mode

This SAM template defines multiple Lambda functions that resides in the current directory (handler.js). It uses Node.js 16x as its runtime, has full access to DynamoDB to perform CRUD operations and is triggered by either GET, POST, PUT or DELETE.

NOTE: As an alternative you can also use Method: ANY for the Path: /car

 MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: handler.handler
      Runtime: nodejs16.x
      Policies: AmazonDynamoDBFullAccess
      Events:
        PostPutDeleteApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /car
            Method: ANY
        HealthCheckApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /check
            Method: GET
        InventoryApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /inventory
            Method: GET

Enter fullscreen mode Exit fullscreen mode

Step 11. We will establish our DynamoDB table along with the corresponding output parameters. For the ProvisionedThroughput settings of our DynamoDB table, we will opt for a 'PAY_PER_REQUEST' pricing model, effectively incurring charges only for the operations we execute. Accordingly, these settings will be configured to 0.

DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties: 
      TableName: sam-table
      AttributeDefinitions: 
        - AttributeName: id
          AttributeType: S
      KeySchema: 
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput: 
        ReadCapacityUnits: 0
        WriteCapacityUnits: 0



Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  OrderApi:
    Description: "API Gateway endpoint URL for Prod stage for Order function"
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/inventory/"
Enter fullscreen mode Exit fullscreen mode

Here is our full template.yml code:


AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"

Parameters:
  GithubRepository:
    Type: String
    Description: GitHub repository URL
  Stage:
    Type: String
    Description: Deployment stage

Globals:
  Function:
    Environment:
      Variables:
        TABLE_NAME: sam-table

Resources:
  AmplifyApp:
    Type: AWS::Amplify::App
    Properties:
      Name: amplify-sam-app
      Repository: !Ref GithubRepository
      IAMServiceRole: !GetAtt AmplifyRole.Arn
      EnvironmentVariables:
        - Name: ENDPOINT
          Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/"

  AmplifyBranch:
    Type: AWS::Amplify::Branch
    Properties:
      BranchName: main
      AppId: !GetAtt AmplifyApp.AppId
      EnableAutoBuild: true

  AmplifyRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - amplify.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: Amplify
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: "amplify:*"
                Resource: "*"

  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Stage
      Cors:
        AllowMethods: "'*'"
        AllowHeaders: "'*'"
        AllowOrigin: "'*'"


  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: handler.handler
      Runtime: nodejs16.x
      Policies: AmazonDynamoDBFullAccess
      Events:
        PostApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /car
            Method: POST
        PutApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /car
            Method: PUT
        DeleteApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /car
            Method: DELETE
        HealthCheckApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /check
            Method: GET
        InventoryApi:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /inventory
            Method: GET


  DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties: 
      TableName: sam-table
      AttributeDefinitions: 
        - AttributeName: id
          AttributeType: S
      KeySchema: 
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput: 
        ReadCapacityUnits: 0
        WriteCapacityUnits: 0



Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  OrderApi:
    Description: "API Gateway endpoint URL for Prod stage for Order function"
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/inventory/"
Enter fullscreen mode Exit fullscreen mode

Step 12. Next, we will generate a handler.js file within our amplify-sam-backend directory. This file is designated to act as our Lambda handler, encompassing its requisite functionalities. Essentially, this is a Node.js script responsible for processing requests from our API Gateway and fetching data from our DynamoDB table via the AWS SDK.

NOTE: I plan to publish an additional blog post that will provide an in-depth explanation of this handler.



const AWS = require('aws-sdk');

const dynamodb = new AWS.DynamoDB.DocumentClient();
const dynamodbTableName = process.env.TABLE_NAME;
const checkPath = '/check';
const carPath = '/car';
const inventoryPath = '/inventory';

exports.handler = async function(event) {
  console.log('Request event: ', event);
  let response;
  switch(true) {
    case event.httpMethod === 'GET' && event.path === checkPath:
      response = buildResponse(200);
      break;
    case event.httpMethod === 'GET' && event.path === carPath:
      response = await getProduct(event.queryStringParameters.id);
      break;
    case event.httpMethod === 'GET' && event.path === inventoryPath:
      response = await getProducts();
      break;
    case event.httpMethod === 'POST' && event.path === carPath:
      response = await saveProduct(JSON.parse(event.body));
      break;
    case event.httpMethod === 'DELETE' && event.path === carPath:
      response = await deleteProduct(JSON.parse(event.body).id);
      break;
    default:
      response = buildResponse(404, '404 Not Found');
  }
  return response;
}

async function getProduct(id) {
  const params = {
    TableName: dynamodbTableName,
    Key: {
      'id': id
    }
  }
  return await dynamodb.get(params).promise().then((response) => {
    return buildResponse(200, response.Item);
  }, (error) => {
    console.error('Get product error: ', error);
  });
}

async function getProducts() {
  const params = {
    TableName: dynamodbTableName
  }
  const allProducts = await scanDynamoRecords(params, []);
  const body = {
    inventory: allProducts
  }
  return buildResponse(200, body);
}

//Recursize function for DynamoDB scan. DynamoDB limit on return on one querey
async function scanDynamoRecords(scanParams, itemArray) {
  try {
    const dynamoData = await dynamodb.scan(scanParams).promise();
    itemArray = itemArray.concat(dynamoData.Items);
    if (dynamoData.LastEvaluatedKey) {
      scanParams.ExclusiveStartkey = dynamoData.LastEvaluatedKey;
      return await scanDynamoRecords(scanParams, itemArray);
    }
    return itemArray;
  } catch(error) {
    console.error('DynamoDB scan error: ', error);
  }
}

async function saveProduct(requestBody) {
  const params = {
    TableName: dynamodbTableName,
    Item: requestBody
  }
  return await dynamodb.put(params).promise().then(() => {
    const body = {
      Operation: 'SAVE',
      Message: 'SUCCESS',
      Item: requestBody
    }
    return buildResponse(200, body);
  }, (error) => {
    console.error('POST error: ', error);
  })
}


async function deleteProduct(id) {
  const params = {
    TableName: dynamodbTableName,
    Key: {
      'id': id
    },
    ReturnValues: 'ALL_OLD'
  }
  return await dynamodb.delete(params).promise().then((response) => {
    const body = {
      Operation: 'DELETE',
      Message: 'SUCCESS',
      Item: response
    }
    return buildResponse(200, body);
  }, (error) => {
    console.error('Delete error: ', error);
  })
}

function buildResponse(statusCode, body) {
  return {
    statusCode: statusCode,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
      'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Credentials': 'true',
      'Access-Control-Allow-Origin': '*',
      'X-Request-With': '*'
    },
    body: JSON.stringify(body)
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 13. Next, we will initiate the deployment of our assets to the cloud using the guided SAM deploy process. This action will result in the creation of our Serverless Application Model (SAM) application.

To commence deployment, navigate to the amplify-sam-backend directory within your terminal and execute the following command:

sam deploy --guided
Enter fullscreen mode Exit fullscreen mode

You will be prompted to enter various parameters:

Stack Name: amplify-sam-backend
AWS region: us-east-1
Parameter GithubRepository: https://github.com/'your github username'/amplify-sam-app
Confirm changes before deploy [Y/n]: Y
Allow SAM CLI role creation [Y/n]: Y
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]: samconfig.toml
SAM configuration environment [default]: 
Enter fullscreen mode Exit fullscreen mode

The SAM CLI will orchestrate the deployment to the cloud, a process that generally requires approximately 2-3 minutes for completion.

Upon successful deployment, navigate to your AWS Management Console and proceed to CloudFormation > amplify-sam-backend > Resources to confirm that the application has been instantiated as expected.

Finally, return to the AWS Management Console and locate AWS Amplify. Identify our application, denoted as "amplify-sam-app," and select it. Once the deployment has successfully concluded, you may click the provided link to verify the application's functionality.

amplify


Next Part 5: Modifying the Front-end

Top comments (0)