Implementing a REST API with AWS (API Gateway, Lambda, and DynamoDB)

Chris Spirito
Level Up Coding
Published in
11 min readApr 9, 2020

--

This is just a short tutorial on how to implement a REST API that uses AWS API Gateway to route requests to a Lambda function (NodeJS) that can query a Database (DynamoDB). This is my first time using all of these AWS services so I’m sure that there are “better” ways to implement this, but as a hacker I chose the path of least resistance. Feedback is of course welcome.

1. Login to your AWS account and Navigate to DynamoDB

2. Create a Table in Dynamo

So we we can store all of our information in a single table for now. Click Create Table and then use these settings:

You can use the default settings under the Table Settings section of the page.

3. Add a record to the Table we created

After you create the table in Dynamo you will be dropped into the Table Admin page. Select the Items Tab and then click Create Item. The User Interface thoughtfully includes the Partition Key that we used to create the Table. It is probably easier to switch to Text mode on the top left and create this object:

{
“UserId”: “id_12345”,
“FirstName”: “Chris”,
“LastName”: “Spirito”
}

Click Save and you will see the new Item in the Table. This is just placed here so we have something to query against once we write the sample code.

4. Create a Lambda Function

This is the code segment that AWS will run for us. whenever we ask it to. What we are going to do is create a REST API Endpoint that Calls the Lambda function to return the data which is queried from the DynamoDB. Click on the AWS icon on the top left and then find Lambda from the list of services and select it. This should bring you to the Lambda Functions view but if it does not just select Functions from the options on the left. Click Create function and use the Author from Scratch option. The Basic Information should look like this:

We are going to update the Permissions separately so you can leave that section as-is for now. Once you click Create Function you will be dropped into the Configuration section of the Lambda function. You should see some code that looks a little big familiar in the Function Code window. That is the code we will be updating to handle our requests.

5. Create a Role in the IAM

We need to create an AWS Role that will allow the Lambda Function to talk to the DynamoDB as well as be allowed to run at all. Let’s create a role with these permissions. Click on the AWS icon again and this time search for IAM from the AWS service list and select it. Select Roles from the options on the left. Click Create role and then make sure that AWS service is selected at the top. Choose Lambda for the use case and then click Next: Permissions. Now is our chance to attach some policies that dictate which service is allowed to do what and who they can talk with. We need to add two services: AWSLambdaBasicExecutionRole and AmazonDynamoDBFullAccess. Search for each of these roles and check the checkbox on the left. Once that is done click Next: Tags. We are not going to add any tags so you can click Next: Review.

Give the role a name. I chose CT_AWS_Role as the Rolle name. You should see the two Policies we added.

The new role should be in your list now:

6. Apply the Role to the Lambda function

Switch back into Lambda via the AWS Console and then choose the Permissions tab where the Execution role is defined. When you create the Lambda function AWS auto-created a role for you, hence the funny name. This role allows the Lambda function to write log entries to CloudWatch which is something we can think about doing later. Change the Execution role to the role that we just created CT_AWS_Role so that we are able to make calls to the DynamoDB. Click Edit and then select the role you created under Existing role.

Click Save and you should be dropped back to the Configuration tab.

7. Test the Lambda Function to see if it can talk to the DynamoDB

Finally we get to write some code! Replace the contents of index.js with this code. The one thing to check for is the AWS region you are attached to. If you look at the upper right hand corner of the AWS browser window you will see your region:

Use the corresponding region name (e.g. us-east-1) in the region attribute in the code:

const AWS = require (‘aws-sdk’);
const TABLE = “CT_Contacts”
const dynamoDb = new AWS.DynamoDB.DocumentClient({
region: ‘us-east-1’
});
exports.handler = async (event) => {

const _id = “id_12345”;

const params = {
TableName: TABLE,
Key: {
“UserId”: _id
}
};

const data = await dynamoDb.get(params).promise();
const response = {
isBase64Encoded: false,
headers: {“Content-Type”: “application/json”}
};

if ((typeof data) === “object”) {
response.statusCode = 200;
response.body = JSON.stringify(data);
} else {
response.statusCode = 500;
}
return response;
};

Save your code by pressing the Save button and then let’s run a test to see if it works. Click on the Test drop-down arrow and select Configure Events. Give the event a name such as TestEvent and just leave the input as-is or you can remove the input complete and leave it as {}:

Click Create and then you will be back at the code edit window. Next click Test and you should see the following in the Execution results window:

If you see the data from the DynamoDB, hooray! Else, we need to troubleshoot a bit to figure out where the issue is.

8. Now let’s create a REST Endpoint that we can use to make calls into our Lambda Function

Navigate to AWS -> API Gateway so we can create our endpoints. It should drop you into the APIs configuration screen but if it does not select APIs from the left bar.

Click on the Create API button.

Scroll down and find REST API and click Build. Use the following settings (make sure you select Edge optimized) for Endpoint Type. Not that it would really matter that much, but since our application is deployed via CloudFlare, we might as well also use Edge Optimization:

Click Create API and you will be dropped into your API Methods section.

Click Actions and then Create Resource which will allow you to create a New Child Resource:

Use the following settings:

Click Create Resource

This will drop you into the ANY section of your Resource setup. This is where you will define the Lambda function that gets called.

When you click Save you will have to verify that you are adding a permission to the Lambda Function to be connected to the API:

This will then drop you into the Resource we created and show you the Method Execution:

Now we have to Deploy the API which will create the endpoint we can call in order to test this. Click Actions and then Deploy API. You are going to have to create a new deployment stage. Use the following settings for now:

Click Deploy

And now we have our URL Endpoint:

For now we don’t need to set any other attributes of this API. Let’s test it out. Open up the URL in the browser:

Oh no! Not what we were expecting! I was hoping to see the data from our Lambda. But not a big deal because we were warned of this when we created the Resource and how if we wanted the root / resource to route we would have to create a separate resource just for that. Just add in, well, anything at the end of the url and you should see what we were expecting:

Very exciting! So now our API is passing through to our Lambda function that is then calling DynamoDB for the data. We are cruising now.

9. Understanding the API URL:

https:// uzrvpextp9.execute-api.us-east-1.amazonaws.com /devel /contacts

https:// : This is known as the Scheme, so in a web browser typically http:// or https:// depending if you are going secure or not.

uzrvpextp9.execute-api.us-east-1. : Subdomains.

amazonaws.com : Second-level and top-level domains.

/devel : This is the API Stage that we deployed to in the last section. This allows us to have different stages in case we want to perform testing in development before pushing to production or another use case (there are many)

/contacts : This is the {proxy+} that we setup in the last section which essentially passes through all of the variables to our Lambda function for us to use. The other option would have been to map individually GET/PUT/DELETE/… API calls to different lambda functions which would likely make sense if our application was more complex, but given what we are trying to accomplish, this should be sufficient.

This is documented here: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html

10. Doing something with the Proxy variables being passed through

So this is where we have to put on our hacker-developer hats a bit because the documentation wasn’t super clear to me, so here is what I did. Now that the Lambda function is working, and the API gateway is working, let’s go back to the Lambda code and make two changes:

Change export.hanlder to:exports.handler = async (event, context, callback) => {Add this line just before the return:response.body = JSON.stringify(event);

This will allow us to see what is being passed into the Lambda so we know how we can grab what we need. We would typically do this using console.log messages but that would require us to go to the CloudWatch logs and this is a bit easier, the hacker way. Reload the page and you will see this:

That is a ton of good information right? But the format is crap. So just open this up under the Network tab, choosing the contacts?id=blah from the Name column and then Preview. This will provide you with this view:

Much easier to work with right?

So let’s implement in our lambda a GET to /contacts where if the user passes in an Id, we will search the database for it and return the result.

So change your URL to:

https://<your_url>/devel/contacts?id=blah

As you can see I added in the ? which indicates that the rest of the URL are query parameters passed in the form of variable_name=value. The reason I used blah above is so that I can easily find it within the results. If I had chosen id=1 or id=a it might have a little tricky to find given the overlap. Looking at the results I see that what we are interested in is:

httpMethod which we will test to make sure it is equal to “GET”

pathParameters.proxy which will give us the /xxx from the URL

queryStringParameters.id which will give us the Id we should search for.

Let’s make sure we can extract these correctly. In the Lambda function add the following lines to the return:

add this line:var _myValues = `${event.httpMethod} — ${event.pathParameters.proxy} — ${event.queryStringParameters.id}`;update this line:response.body = _myValues;

And the refresh the page and you should see this:

So here is a challenge and a solution is included below. Rewrite the Lambda function to:

  • Accept GET requests for /contacts where an id variable is passed in. Example: /contacts?id=id_12345
  • Return Status Code 200 OK
  • Return the Object from the Database in the Body
  • If the path is not /contacts return Status Code 404 Not Found
  • If the path is /contacts but the method is not GET return Status Code 405 Method Not Allowed
  • If there is a database error (the data returned from the database is not of type object) then return Status Code 500 Internal Server Error

A Solution:

const AWS = require ('aws-sdk');
const TABLE = "CT_Contacts"

const dynamoDb = new AWS.DynamoDB.DocumentClient({
region: 'us-east-1'
});

// Reference for HTTP Response Codes
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

exports.handler = async (event, context, callback) => {

// Set variables
var _httpMethod = event.httpMethod;
var _proxyPath = event.pathParameters.proxy;
var _queryStringParameters = event.queryStringParameters == null ? {} : event.queryStringParameters;

// Required format for data that is returned via a AWS API Proxy
// The four fields required are:
// isBase64Encoded, headers, statusCode, body

const response = {
isBase64Encoded: false,
headers: {"Content-Type": "application/json"},
body: ""
};

// If _proxyPath is contacts proceed, else return 404 Not Found

if (_proxyPath == "contacts") {
// Right now we only allow GET Requests for contacts path
// If the request is good return 200 OK
// If the request not a GET, return 405 Method not Allowed
if (_httpMethod == "GET") {
// Grab the Id
var _id = _queryStringParameters.hasOwnProperty("id") ? _queryStringParameters.id : "";

// Setup the DynamoDB Input Variables
const params = {
TableName: TABLE,
Key: {
"UserId": _id
}
};

// Make the call to DynamoDB using await for the promise
// since the usual approach never executes for some
// reason

const data = await dynamoDb.get(params).promise();

// Check to see that the DB returned something useful

if ((typeof data) === "object") {
response.statusCode = 200;
response.body = JSON.stringify(data);
} else {
response.statusCode = 500;
}
} else {
response.statusCode = 405;
}
} else {
response.statusCode = 404;
}

return response;
};

--

--