DEV Community

Cover image for Serverless backend setup for a todo list in AWS
Shakir for AWS Community Builders

Posted on • Updated on

Serverless backend setup for a todo list in AWS

Hello πŸ‘‹, in this post πŸ“ we are going to build a simple backend for a todo application using AWS services ☁️ such as AWS API Gateway, AWS Lambda and AWS DynamoDB.

Starting with DynamoDB, we are gonna setup a table with task as the hash / partition keyπŸ”‘. This table is a simple one with four fields task, description, date and done(binary). We don't have to specify all the fields while creating the table though except the hash key, the fields could be created dynamically while we add data to the table.
Create database table

Leave rest of the settings to the default and create the table.

Next, lets go to IAM and create a policy πŸ“ƒ which would allow CRUD operations on our DynamoDB table todo. I have set CrudOhioDynamoTodoTable as the policy name as Ohio(us-east-2) is the region where the table was created. You may visit this site to generate policies🚦. The policy json is as follows.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "dynamodb:DeleteItem",
                "dynamodb:GetItem",
                "dynamodb:PutItem",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:dynamodb:us-east-2:<account-id>:table/todo"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

IAM policy created

We have to now create a role and attach this policy.
IAM role creation

Select the use case as Lambda and on the next page select the policy we have created.
Attach roleto policy

Choose a name for the Role, for ex. LambdaToCrudOhioDynamoTodoTable, and finally create the role.
Role created

Progress so far: Lambda service role > Crud Policy > DynamoDB

So the IAM part is done, we are good to create the Lambda function now and attach the role to it. Just set a function name (ex. todo) and set the execution role to the role we created. Rest of the settings can be default.
Create Lambda function

Replace the code in index.js with the following and deploy the function.

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

const dynamo = new AWS.DynamoDB.DocumentClient();

exports.handler = async (event, context) => {
  let body;
  let statusCode = 200;
  const headers = {
    "Content-Type": "application/json"
  };

  try {
    switch (event.routeKey) {
      case "DELETE /tasks/{task}":
        await dynamo
          .delete({
            TableName: "todo",
            Key: {
              task: event.pathParameters.task
            }
          })
          .promise();
        body = `Deleted task ${event.pathParameters.task}`;
        break;

      case "GET /tasks/{task}":
        body = await dynamo
          .get({
            TableName: "todo",
            Key: {
              task: event.pathParameters.task
            }
          })
          .promise();
        break;

      case "GET /tasks":
        body = await dynamo.scan({ TableName: "todo" }).promise();
        break;

      case "PUT /tasks":
        let requestJSON = JSON.parse(event.body);
        await dynamo
          .put({
            TableName: "todo",
            Item: {
              task: requestJSON.task,
              description: requestJSON.description,
              date: requestJSON.date,
              done: requestJSON.done ? true : false
            }
          })
          .promise();
        body = `New task added: ${requestJSON.task}`;
        break;

      default:
        throw new Error(`Unsupported route: "${event.routeKey}"`);
    }
  } catch (err) {
    statusCode = 400;
    body = err.message;
  } finally {
    body = JSON.stringify(body);
  }

  return {
    statusCode,
    body,
    headers
  };
};
Enter fullscreen mode Exit fullscreen mode

Ok so our Lambda function is ready.
Lambda Function > Lambda Role > Crud Policy > Dynamo DB

What's next, something should invoke the Lambda, which is our API. Let's create a single API gateway with relevant API methods.

Go to API gateway on the console, and create an HTTP API with lambda integration.
Create API gateway

And next the routes.
Configure API gateway routes

The gateway can be created with other prompts set to default values.
API gateway created

Make a note of the execution URL, as that would be used to make API calls.

I am going to better set it as an environment variable, so that I don't have to keep typing the longer URL for the rest of the post.

$ executionURL=https://j6yotbi8ta.execute-api.us-east-2.amazonaws.com
Enter fullscreen mode Exit fullscreen mode

Hmm ok, so I think our backend is kinda setup now with this.
API gateway > Lambda Function > Lambda Role > CRUD policy > DynamoDB

Let's try making some calls and see if it actually works.

$ curl -X PUT ${executionURL}/tasks -H "Content-Type: application/json" -d '{                        
"task": "BuyBooks",
"description": "Buy Java and System Design books from Amazon",
"date": "10-Apr-2022"
}'
"New task added: BuyBooks"

 $ curl -X PUT ${executionURL}/tasks -H "Content-Type: application/json" -d '{
"task": "ShutEC2Instances",
"description": "Shut down the unused EC2 instances to avoid billing",                                      
"date": "10-Apr-2022"              
}'
"New task added: ShutEC2Instances"
Enter fullscreen mode Exit fullscreen mode

We were able to successfully add couple new tasks with the PUT method.

Let's retrieve all items from the DB table.

$ curl --silent ${executionURL}/tasks | jq                                                            
{
  "Items": [
    {
      "date": "10-Apr-2022",
      "task": "ShutEC2Instances",
      "description": "Shut down the unused EC2 instances to avoid billing",
      "done": false
    },
    {
      "date": "10-Apr-2022",
      "task": "BuyBooks",
      "description": "Buy Java and System Design books from Amazon",
      "done": false
    }
  ],
  "Count": 2,
  "ScannedCount": 2
}
Enter fullscreen mode Exit fullscreen mode

And now, retrieve items based on the primary key which in our case is task.

$ curl --silent ${executionURL}/tasks/BuyBooks | jq
{
  "Item": {
    "date": "10-Apr-2022",
    "task": "BuyBooks",
    "description": "Buy Java and System Design books from Amazon",
    "done": false
  }
}

$ curl --silent ${executionURL}/tasks/ShutEC2Instances | jq                                               
{
  "Item": {
    "date": "10-Apr-2022",
    "task": "ShutEC2Instances",
    "description": "Shut down the unused EC2 instances to avoid billing",
    "done": false
  }
}
Enter fullscreen mode Exit fullscreen mode

Lets make couple changes to the tasks. For the first one Java is replaced with JavaScript, and the second one I'm setting done as true.

$ curl -X PUT https://j6yotbi8ta.execute-api.us-east-2.amazonaws.com/tasks -H "Content-Type: application/json" -d '{
"task": "BuyBooks",
"description": "Buy JavaScript and System Design books from Amazon",                                                                                  
"date": "10-Apr-2022"
}'
"New task added: BuyBooks"

$ curl -X PUT https://j6yotbi8ta.execute-api.us-east-2.amazonaws.com/tasks -H "Content-Type: application/json" -d '{
"task": "ShutEC2Instances",
"description": "Shut down the unused EC2 instances to avoid billing",
"date": "10-Apr-2022", "done": "true"
}'
"New task added: ShutEC2Instances"
Enter fullscreen mode Exit fullscreen mode

The put method either creates a new item, or replaces an existing item, which I think is fine in our case as our table is kinda small. Let's view the contents one more time.

$ curl --silent https://j6yotbi8ta.execute-api.us-east-2.amazonaws.com/tasks | jq                                                                
{
  "Items": [
    {
      "date": "10-Apr-2022",
      "task": "ShutEC2Instances",
      "description": "Shut down the unused EC2 instances to avoid billing",
      "done": true
    },
    {
      "date": "10-Apr-2022",
      "task": "BuyBooks",
      "description": "Buy JavaScript and System Design books from Amazon",
      "done": false
    }
  ],
  "Count": 2,
  "ScannedCount": 2
}
Enter fullscreen mode Exit fullscreen mode

The value for "done" is boolean though we have set it as string while executing the curl command, this is due to the following code in Lambda function.

done: requestJSON.done ? true : false
Enter fullscreen mode Exit fullscreen mode

Finally, the delete method.

$ curl  -X DELETE ${executionURL}/tasks/ShutEC2Instances                                                                                                    "Deleted task ShutEC2Instances"

$ curl  -X DELETE ${executionURL}/tasks/BuyBooks
"Deleted task BuyBooks"
Enter fullscreen mode Exit fullscreen mode

So we have been sending unauthenticated calls from CURL. For some security, we can add IAM authorizer to all the routes.
Attach IAM authorizer to routes

With this, we shouldn't be able to make calls like before, we should now be authenticated and authorized.

$ curl ${executionUrl}/tasks |  jq                                                                                       
curl: (3) URL using bad/illegal format or missing URL
Enter fullscreen mode Exit fullscreen mode

Let's use postman as its easy to send requests from it with AWS signature.
Get request in postman

The IAM user whose credentials are entered should have permissions to send API calls to the API gateway. In my case the user id has admin permissions and hence it works good.
Response in postman

Well that's it for this post.. πŸ‘ So we saw how to setup the backend for a mini todo application with API gateway, Lambda and IAM. And we tested it with a few API calls via CURL(with out authentication) and Postman(with IAM authentication).

But what's pending, we could have some sort of a web frontend that could send API calls to the API gateway using client libraries such as fetch, axios etc. just like CURL / Postman did it for us from the CLI / Desktop, thus resulting in a proper web application...

Thanks for reading !!! Don't forget to delete the resources you have created, if they are no longer in use :)

Top comments (0)