Building a Serverless CRUD API with AWS: A Comprehensive Guide
Tags: devops, aws, cloudcomputing, serverless
Published: 2024-07-09 06:39:14

**Introduction**

In the era of cloud computing, serverless architectures have become a game-changer, allowing developers to build and deploy applications without worrying about the underlying infrastructure. AWS offers a powerful suite of services that make it easy to create serverless applications. In this blog, we'll walk you through building a serverless CRUD (Create, Read, Update, Delete) API using AWS Lambda, API Gateway, IAM, and DynamoDB.

**Why Choose Serverless?**

Serverless computing offers several advantages:

- Scalability:
 Automatically scales with the demand.

- Cost-Efficiency:
 Pay only for what you use.

- Reduced Operational Overhead:
 Focus on code rather than infrastructure.

- High Availability:
 Built-in redundancy and fault tolerance.

**Components of Our Serverless CRUD API**

- API Gateway:
 Acts as the entry point for your API.

- AWS Lambda:
 Executes your business logic.

- IAM:
 Manages access and permissions.

- DynamoDB:
 A NoSQL database for storing your data.

- Postman software:
 A software that is used to write code and send requests to the API Gateway
 
**Step-by-Step Guide**

1. Set Up DynamoDB
Create a DynamoDB table named product-inventory with productid as the partition key.

2. Create Lambda Functions
Create Lambda functions for each CRUD operation. Here's an example of a Lambda function for handling all CRUD operations:



3. Deploy Lambda Functions
Deploy your Lambda functions using the AWS Management Console or AWS CLI.
Here is a sample code:

```
const AWS = require('aws-sdk');
AWS.config.update( {
  region: 'us-east-1'
});
const dynamodb = new AWS.DynamoDB.DocumentClient();
const dynamodbTableName = 'product-inventory';
const healthPath = '/health';
const productPath = '/product';
const productsPath = '/products';

exports.handler = async function(event) {
  console.log('Request event: ', event);
  let response;
  switch(true) {
    case event.httpMethod === 'GET' && event.path === healthPath:
      response = buildResponse(200);
      break;
    case event.httpMethod === 'GET' && event.path === productPath:
      response = await getProduct(event.queryStringParameters.productid);
      break;
    case event.httpMethod === 'GET' && event.path === productsPath:
      response = await getProducts();
      break;
    case event.httpMethod === 'POST' && event.path === productPath:
      response = await saveProduct(JSON.parse(event.body));
      break;
    case event.httpMethod === 'PATCH' && event.path === productPath:
      const requestBody = JSON.parse(event.body);
      response = await modifyProduct(requestBody.productid, requestBody.updateKey, requestBody.updateValue);
      break;
    case event.httpMethod === 'DELETE' && event.path === productPath:
      response = await deleteProduct(JSON.parse(event.body).productid);
      break;
    default:
      response = buildResponse(404, '404 Not Found');
  }
  return response;
}

async function getProduct(productid) {
  const params = {
    TableName: dynamodbTableName,
    Key: {
      'productid': productid
    }
  }
  return await dynamodb.get(params).promise().then((response) => {
    return buildResponse(200, response.Item);
  }, (error) => {
    console.error('Do your custom error handling here. I am just gonna log it: ', error);
  });
}

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

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('Do your custom error handling here. I am just gonna log it: ', 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('Do your custom error handling here. I am just gonna log it: ', error);
  })
}

async function modifyProduct(productid, updateKey, updateValue) {
  const params = {
    TableName: dynamodbTableName,
    Key: {
      'productid': productid
    },
    UpdateExpression: `set ${updateKey} = :value`,
    ExpressionAttributeValues: {
      ':value': updateValue
    },
    ReturnValues: 'UPDATED_NEW'
  }
  return await dynamodb.update(params).promise().then((response) => {
    const body = {
      Operation: 'UPDATE',
      Message: 'SUCCESS',
      UpdatedAttributes: response
    }
    return buildResponse(200, body);
  }, (error) => {
    console.error('Do your custom error handling here. I am just gonna log it: ', error);
  })
}

async function deleteProduct(productid) {
  const params = {
    TableName: dynamodbTableName,
    Key: {
      'productid': productid
    },
    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('Do your custom error handling here. I am just gonna log it: ', error);
  })
}

function buildResponse(statusCode, body) {
  return {
    statusCode: statusCode,
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  }
}
```

4. Set Up API Gateway
Create an API:

- 
Choose REST API.

- 
Set up methods (GET, POST, PATCH, DELETE) for your resources (/health, /product, /products).

- 
Integrate each method with the respective Lambda function.

- 
Enable CORS and enable proxy for the resources

- 
Configure CORS settings for each method.

- 
Deploy the API:

- 
Create a new deployment stage (e.g., dev, prod).

5. Configure IAM Roles
Ensure your Lambda functions have the necessary permissions to interact with DynamoDB. Create an IAM role with policies that allow dynamodb to perform actions on your table.

6. Testing the API
Use tools like Postman or CURL to test your API endpoints. Ensure each CRUD operation works as expected:

GET /health: Check API health.
GET /product?productid=10001: Retrieve a specific product.
GET /products: Retrieve all products.
POST /product: Create a new product.
PATCH /product: Update an existing product.
DELETE /product: Delete a product.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a78hwfg779fgxdxwxub5.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nni1e6cwu2ve6rpcff4a.png)

**Benefits of Using Serverless CRUD API**

- 
Scalability: Automatically scales with demand.

- 
Cost-Efficiency: Pay only for what you use.

- 
Reduced Operational Overhead: No need to manage servers.

- 
High Availability: Built-in redundancy and fault tolerance.

- 
Security: Fine-grained access control with IAM.

**Conclusion**

Building a serverless CRUD API using AWS Lambda, API Gateway, IAM, and DynamoDB is an excellent choice for scalable, cost-effective, and secure backend solutions. With the steps outlined in this blog, you can quickly set up your own serverless API and start leveraging the power of AWS for your applications. 

Happy coding!