Building Serverless REST APIs on AWS: A Step-by-Step Guide with Lambda, API Gateway, and DynamoDB
Tags: aws, restapi, lambda, dynamodb
Published: 2024-01-29 14:55:19


Hello **Devs**,

Today we are going to see how we can use AWS serverless infrastructure to create RESTAPIs. This is just basic demo, we cant use it in project but can be used for mini projects. Also this is in free tier 🤟🏻

lets start with basic intro of all the service we are going to use.

## 1. AWS lambda function
 
- AWS Lambda is a serverless computing service provided by Amazon Web Services (AWS). Serverless computing allows you to run code without provisioning or managing servers. With AWS Lambda, you can execute your code in response to specific events, such as changes to data in an Amazon S3 bucket, updates to a DynamoDB table, or an HTTP request via API Gateway.

More information : [here](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html).

## 2. API Gateway

- Amazon API Gateway is a fully managed service provided by Amazon Web Services (AWS) that allows you to create, deploy, and manage APIs (Application Programming Interfaces) at scale. It acts as a gateway between your applications and the backend services, enabling you to create RESTful or WebSocket APIs.

More information : [here]((https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html))

## 3. DynamoDB

- Amazon DynamoDB is a fully managed NoSQL database service provided by Amazon Web Services (AWS). It is designed to provide fast and predictable performance with seamless scalability. DynamoDB is suitable for a variety of use cases, ranging from simple key-value stores to more complex applications with high read and write throughput requirements.

More information : [here](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html)


## Now lets start by doing  🤞🏼


![Flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xfvo6pifujfzgbi5sb2r.png)

**STEPS**
1. Create a Permission Policy 
2. Create Execution Role for Lambda function
3. Create Function
4. Create RESTAPI using API Gateway
5. Create Resources on Your RESTAPI
6. Create a DynamoDB table 
7. Write and understand Code
8. Test the integration of API Gateway, Lambda, and DynamoDB.

**Optional**
9.Deploy (not on your domain because idk how to yet😉)

## **1. Create a Permission Policy**
- Go to Your AWS console and search IAM 

![search iam](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4qttnnwb2nb31zx6qlmo.png)

- Click on **Polices**

![policies](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bmul4azt3z74crzirsbf.png)

- Create **New Policy**
- Select JSON and paste the following code 


```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Action": [
        "dynamodb:DeleteItem",
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Query",
        "dynamodb:Scan",
        "dynamodb:UpdateItem"
      ],
      "Effect": "Allow",
      "Resource": "*"
    },
    {
      "Sid": "",
      "Resource": "*",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Effect": "Allow"
    }
  ]
}
```
- So what does it says, it says to whichever **service** it is attached to will have **access** to **DynamoDB** and to **create Logs**.
- Click next and give name to it, I am giving **lambda-apigateway-policy**
- You can also add tags if you can.

##2. Create Execution Role for Lambda function##

- Click on **roles** 
![Roles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/66rj41eujze911hyzi0g.png)
- Click on create new role 
- Select AWS service and and Service use case as **lambda** and click next

![attach policy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zbgj5eq4zaxoicjst4vr.png)
- Click next and **search** the Policy we created earlier 

![search poilcy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qqkau2ybem8585lnb8ze.png)

- Click next and name the role with any name as per your ease of understanding, i am giving **lambda-apigateway-role**.

## 3. Create Function

- Search **lambda** 
- Click Functions and create.

![Click Functions and create one](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wtdyy5mn24thjyf5qsv3.png)

- Give name of function I am naming **LambdaFunctionOverHttps** 
- **Runtime environment as Node.js 18.x**
- Select **role** we created earlier

![selecting role](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kylzxj7l03xgdopn2djp.png)

- Leave everything **default** and create lambda function

- Will create the function at last because will try to understand it little.

## 4. Create RESTAPI using API Gateway

- Search **API gateway **
- In APIs section create new API with **RESTAPI**(**Not private one**)
- Select new API and name it I am naming it **DynamoDBOperations**
- Leave everything **default** 

![Search API gateway](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wq9peqj4nuf8zj3r2nbf.png)

## 5. Create Resources on Your RESTAPI
 

![Click on create resources](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wnttbhzekqkj9f6k6l61.png)

- Name it, i am naming **getallusers**. 

![Name it i am giving getallusers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xba191jyspoxj5oix799.png)

- Click on **getallusers**, make sure its **highlighted** then click create method.

![Click on getallusers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vv3chpmiultvjbbrsn7f.png)

- In create method select the method type as **GET**.
- Also make sure you select **lambda function** we created earlier and enable **<u>lambda proxy integration</u>**.
- With **Lambda Proxy Integration**, the Lambda function receives the entire request object from the API Gateway, including information such as headers, query parameters, path parameters, and the request body. The Lambda function then processes the request and returns a response object, which includes the status code, headers, and the response body.

![ method select](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/840p72514d4p4k527b3o.png)

- Now we will create another **endpoint** or say **resources** with four method **GET POST PUT DELETE**, i am naming as **user** make sure you have **lambda proxy integration enabled**, it will look something like this.

![endpoints](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p2gp078otrsezmm81bjj.png)

## 6. Create a DynamoDB table
- Search **DynamoDB**.
- Click on **Tables**.
- Create a Table with whichever name you want I am naming **Users**
- Set **Partition key** as **<u>id</u>** with type **<u>String</u>**.
- Leave everything **default** and click on create table.

![Create Table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n2eiw83oizor40nrs2jv.png)

## 7. Write and understand Code
- You can find **code** here : [repo](https://github.com/harshgajjar02/RESTAPIs-lambda-apigateway-dynamodb.git)

- lets start with installing node modules and AWS SDK(Software development kit)
- We need following modules 
  1. **aws-sdk/lib-dynamodb**. More Details [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/) and [here](https://www.npmjs.com/package/@aws-sdk/lib-dynamodb)
  2. **aws-sdk/client-dynamodb**. More Details [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/) and [here](https://www.npmjs.com/package/@aws-sdk/client-dynamodb).

**Lets start understanding code**
- First we **Import** what we need 

```
import { DynamoDBDocumentClient, PutCommand, GetCommand, UpdateCommand, DeleteCommand, ScanCommand } from "@aws-sdk/lib-dynamodb";

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";

const region = "us-east-1";
const ddbClient = new DynamoDBClient({ region: region });
const ddbDocClient = DynamoDBDocumentClient.from(ddbClient);
const tablename = "Users";
```
- First we create **ddbClient** , **DynamoDBClient** with specified **AWS region**. This **client** will be used to interact with **DynamoDB**
- The **DynamoDBDocumentClient** is a higher-level client provided by the AWS SDK for JavaScript. It simplifies working with DynamoDB by providing a more JavaScript-friendly API, and it is built on top of the lower-level **DynamoDBClient**. It is created from the previously created **DynamoDBClient** instance.
- Please add your region in which you have created all the resources. And also tablename.

```
export const handler = async (event) => {
..
..
..
}
```
- This Block is provided by **lambda function** in which we can respond to specific event.
- **Event** is an **object** which is returned by lambda function if we enable **lambda proxy** during we created method in API resource creation section.
- If you want to see what that contains then just return event in response, will see later.

- Here we have specified two paths **getalluserPath** and **userpath** as per our endpoints.
- We call a specific **javascript function** based on **httpMethod** and **endpoint path**.
- We get details of both from the **event object**.
- You should once return this event object as a response so that whole concept will be much clearer.
- Make sure you 
```
export const handler = async (event) => {
  let response;

  const getallusersPath = "/getallusers";
  const userpath = "/user";
  const body = JSON.parse(event.body);
  // event.payload.TableName = tablename;
  switch (true) {
    case event.httpMethod === "GET" && event.path === getallusersPath:
      response = getallusers();
      break;
    case event.httpMethod === "GET" && event.path === userpath:
      response = getSingleuser(event.queryStringParameters.id);
      // response = buildResponse(200, "hello there");
      break;
    case event.httpMethod === "POST" && event.path === userpath:
      // response = buildResponse(200 , event.queryStringParameters.id);
      response = saveUser(body);
      break;
    case event.httpMethod === "PUT" && event.path === userpath:
      response = updateUser(body);
      break;
    case event.httpMethod === "DELETE" && event.path === userpath:
      // response = buildResponse(200,bod1.payload);
      response = deleteUser(body.payload);
      // response = buildResponse(200, "hello there");
      break;
    default:
      response = buildResponse(404, "404 Not Found");
  }
  return response;
};
```

- I will explain one function and rest is same.
- What we are doing here is creating an object of the required method and then sending that object to client, we created earlier so that it can execute our query.
- lets take example of **saveUser** function which saves the user in the **Dynamodb**.
- Here i have specific structure of request body. 
- The id is user specified right now but it should be **Dynamically** created Sorry for that 😶
```
{
    "payload" : {
        "Item": {
            "id": "1255",
            "username":"Bruce Wayne ",
            "description":"I love Vadapav :)"
        }
    }
}
```
- Anything we add under the **Item** section will be added to database
- In **PutCommand** object below we have three keys one is specifying **TableName**, **Item** to be added, and a **ConditionExpression** in which it is define a condition that if **<u>id</u>** is already present than dont add it and return **400** error code.
- There are plenty of such conditions **DynamoDB** has.

More Information : [here](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html) 
```
async function saveUser(body) {
  if (!body.payload) {
    return buildResponse(400, "payload not found");
  } else {
    const command = new PutCommand({
      TableName: tablename,
      Item: body.payload.Item,
      ConditionExpression: "attribute_not_exists(id)",
    });

    try {
      let response = await ddbDocClient.send(command);
      return buildResponse(200, response);
    } catch (error) {
      return buildResponse(500, error);
    }
  }
}
```
- Each command object has a specific format to be followed majority times. 
- The **update** command will be somewhat difficult to understand because as we have **NoSQL** database, i made a dynamic query means as we have no specific schema any record can have different attributes so this will automatically handle any number of update attributes or say columns

```
const updateCommand = new UpdateCommand({
       TableName: "YourTableName",
       Key: {
         id: "1234ABCD", // Assuming "id" is the primary key
      },
      UpdateExpression: "SET firstName = :newFirstName, lastName = :newLastName, age = :newAge",
       ExpressionAttributeValues: {
         ":newFirstName": "John",
         ":newLastName": "Doe",
         ":newAge": 30,
       },
       ReturnValues: "ALL_NEW",
     });
```
- **UpdateExpression** contains all the attribute one want to update.
- **ExpressionAttribute** contains the value of attribute to change;
- **ReturnValues** return the response of newly updated record.
- If you want to understand more in detail go through document or videos but please make sure you watch latest video because AWS changes many things and you will get errors. I suggest documentation much more.
- This is all now we will test it 

## 8. Test the integration of API Gateway, Lambda, and DynamoDB
- Copy the code and paste it in index.mjs of lambda function.

![code upload](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pvccjddsung3r16rpqe1.png)

- If you getting error because of node module then upload the zip file containing all the files.

- **Go to APIGateway and click on the resource method of specific endpoint let us start with getalluser.
**
- Make sure of your resource path.
- Mine is somewhat different because i nested all above paths under DynamoDBManger.
- so my path is **/DynamoDBManager/getallusers**(yours should be /getallusers).
- And my **index.mjs** file also has same path as above so make sure to update as per your resource.
![path](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejvowdjaep9lebry5t77.png)

- Click on **Test** section 

![getalluser](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/47maz6kxphvvv89ltybd.png)
- Click on **Test** and you will get **response** along with the some logs
- You wont get any response as your table doesn't have any data

![response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2m93rkj061xpzqpevgi7.png)

- Now lets create some **records** 
- Click on **POST** of /user resource and go to test section
- In request body paste following **JSON**
- Here **id** is mandatory and **attributes** can be anything.

```
{
    "payload" : {
        "Item": {
            "id": "1255",
            "username":"MS dhoni",
            "description":"Best of all time"
        }
    }
}
```

![post response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tmocrr00o0qx0nt5bsry.png)

- You will get following response if **id** already **exists**
- Here you can see we have **ConditionalCheckFailedException** because we had condition mentioned in our code that id should not exist in table

![condition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gj6s3hy4y0c10ijmjz1t.png)

**- GET user with id we created**
- For **GET** request we are using Query strings with id to our table id 

![Get user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/csxu4uy4ota250vy8dcb.png)

- Will get following response if **id** is not available.

![id](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lmrxbpe9dswmls9j2kvc.png)

**- UPDATE the user with id we created**
- For **update** use the following structure or JSON 

```
{
    "payload" : {
       "id":"1200",
       "attributes" : {
           "username":"Sachin Tendulkar"
       }
 }
}
```
- Here attribute name should be same as the original one and the value you wanna update can be different.

![put response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tmpjezftp7xhct863qw5.png)
- You can **explore** what errors you can have.

## 9.Deploy
- Go to **APIgateway** and select your **API** and than click **deploy**. 
- Select NEW stage, name it **dev** or anything.
- Now select the **method** and you will get the link

![deploy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g167h6nylk7iddulxlpf.png)
- You can use **postman** on this links
- By deploying you will get **link** which can be accessed by public.
- I recommend don't share it yet.

##🍰 Conclusion 🍧
- If you reached here than thank you for reading my naive blog.
- It took me 2 days to create this because I used old AWS-SDK on node20 runtime environment😂 and got many errors. 
- Also I used same lambda function for all APIs which not good at all.
- Again I exposed ID to user which is not good, so I did some wrong practice which is not right and will update this in next project in which will try to integrate the AWS Cognito along with this. 
- There is auto-incrementing attribute for id in DynamoDB you can modify and add it to your code as a part of exploration
- Avoid Hardcoding some variable like DB name, AWS-region instead consider using environment variables or AWS Systems Manager Parameter Store to manage such configurations.
- This is overview project to get familiar with AWS services.
- If I did something wrong tell me so can avoid it later.
- console.log("See you later 😉");