visit
config – This folder will hold all the config related files, in our case, it is holding a single file that is creating a DynamoDB AWS SDK instance to use everywhere in our project, so instead of importing DynamoDB instance in each file, we are just importing it in one file and then exporting the instance from this file and importing everywhere else.
functions – This is for holding all the files related to any utility function.
post – This is our main folder which will hold all the lambda functions for our CRUD operations.
service: dynamodb-crud-api
provider:
name: aws
runtime: nodejs12.x
environment:
DYNAMO_TABLE_NAME: PostsTable
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMO_TABLE_NAME}"
Here we are defining one environment variable which will store the name of our DynamoDB table and we are also adding different permissions which our lambda functions will need to do different operations like dynamodb:GetItem
to get the data item from the table, dynamodb:PutItem
to insert a new entry in the table and so on.
functions:
listPosts:
handler: post/list.listPosts
events:
- http:
path: posts/list
method: get
cors: true
createPost:
handler: post/create.createPost
events:
- http:
path: post/create
method: post
cors: true
getPost:
handler: post/get.getPost
events:
- http:
path: post/{id}
method: get
cors: true
updatePost:
handler: post/update.updatePost
events:
- http:
path: post/update
method: patch
cors: true
deletePost:
handler: post/delete.deletePost
events:
- http:
path: post/delete
method: delete
cors: true
path – This is the relative path of the endpoint which we want to use, so for example, if our API Gateway URL is //abc.com then getPost
lambda function will be called with this endpoint //abc.com/post/{id}.
method – This is just the API request type POST, GET, DELETE, etc.
resources:
Resources:
UsersDynamoDbTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Retain
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: ${self:provider.environment.DYNAMO_TABLE_NAME}
AttributeDefinitions – Here we define all the key fields for our table and indices.
KeySchema – Here we set any field which we defined in AttributeDefinitions as Key field, either sort key or partition key.
ProvisionedThroughput – Here we define the number of read and write capacity units for our DynamoDB table.
service: dynamodb-crud-api
provider:
name: aws
runtime: nodejs12.x
environment:
DYNAMO_TABLE_NAME: PostsTable
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMO_TABLE_NAME}"
functions:
listPosts:
handler: post/list.listPosts
events:
- http:
path: posts/list
method: get
cors: true
createPost:
handler: post/create.createPost
events:
- http:
path: post/create
method: post
cors: true
getPost:
handler: post/get.getPost
events:
- http:
path: post/{id}
method: get
cors: true
updatePost:
handler: post/update.updatePost
events:
- http:
path: post/update
method: patch
cors: true
deletePost:
handler: post/delete.deletePost
events:
- http:
path: post/delete
method: delete
cors: true
resources:
Resources:
UsersDynamoDbTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Retain
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: ${self:provider.environment.DYNAMO_TABLE_NAME}
const AWS = require("aws-sdk");
const dynamo = new AWS.DynamoDB.DocumentClient();
module.exports = dynamo;
const sendResponse = (statusCode, body) => {
const response = {
statusCode: statusCode,
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true
}
}
return response
}
module.exports = {
sendResponse
};
We will call this sendResponse
function from many places, in fact from all our lambda functions to return the response for a request, this will return the JSON response back to the user, it has two arguments, one is the HTTP status code and the other is the JSON body which we will pass whenever we are going to call this function, we are also passing some required headers with the response which handles the most common “access not allowed” cors issues.
This lambda function is defined inside the create.js file, in this lambda function, we will be doing our first operation which is inserting a new data item in the table, let’s break it down into parts.
const dynamoDb = require("../config/dynamoDb");
const { sendResponse } = require("../functions/index");
const uuidv1 = require("uuid/v1");
We need to import our DynamoDB instance from the config file which we created earlier, our sendReponse
function and we are using an NPM called uuid
which is used to generate a random id, this id will be used as our partition key for each post.
const body = JSON.parse(event.body);
try {
const { postTitle, postBody, imgUrl, tags } = body;
const id = uuidv1();
const TableName = process.env.DYNAMO_TABLE_NAME;
const params = {
TableName,
Item: {
id,
postTitle,
postBody,
imgUrl,
tags
},
ConditionExpression: "attribute_not_exists(id)"
};
Here we are getting different properties from the request payload which we are going to insert in our post table, after that we are generating a random id by calling a function provided by uuid
library.
attribute_not_exists – By default DynamoDB PutItem will overwrite the content of any item if we are trying to insert data with the same partition key, but we don’t want that so to only insert the data if the partition key is not found we are using this conditional expression.
await dynamoDb.put(params).promise();
return sendResponse(200, { message: 'Post created successfully' })
Whole create.js file
"use strict";
const dynamoDb = require("../config/dynamoDb");
const { sendResponse } = require("../functions/index");
const uuidv1 = require("uuid/v1");
module.exports.createPost = async event => {
const body = JSON.parse(event.body);
try {
const { postTitle, postBody, imgUrl, tags } = body;
const id = uuidv1();
const TableName = process.env.DYNAMO_TABLE_NAME;
const params = {
TableName,
Item: {
id,
postTitle,
postBody,
imgUrl,
tags
},
ConditionExpression: "attribute_not_exists(id)"
};
await dynamoDb.put(params).promise();
return sendResponse(200, { message: 'Post created successfully' })
} catch (e) {
return sendResponse(500, { message: 'Could not create the post' });
}
};
This lambda function is defined inside get.js file, this will be doing the reading operation, meaning getting the data from the DynamoDB using the partition key.
const { id } = event.pathParameters;
const params = {
TableName: process.env.DYNAMO_TABLE_NAME,
KeyConditionExpression: "id = :id",
ExpressionAttributeValues: {
":id": id
},
Select: "ALL_ATTRIBUTES"
};
const data = await dynamoDb.query(params).promise();
if (data.Count > 0) {
return sendResponse(200, { item: data.Items });
} else {
return sendResponse(404, { message: "Post not found" });
}
Whole get.js file
"use strict";
const { sendResponse } = require("../functions/index");
const dynamoDb = require("../config/dynamoDb");
module.exports.getPost = async event => {
try {
const { id } = event.pathParameters;
const params = {
TableName: process.env.DYNAMO_TABLE_NAME,
KeyConditionExpression: "id = :id",
ExpressionAttributeValues: {
":id": id
},
Select: "ALL_ATTRIBUTES"
};
const data = await dynamoDb.query(params).promise();
if (data.Count > 0) {
return sendResponse(200, { item: data.Items });
} else {
return sendResponse(404, { message: "Post not found" });
}
} catch (e) {
return sendResponse(500, { message: "Could not get the post" });
}
};
This lambda is defined inside update.js file, in this lambda function we are going to do the update operation which will update the data inside the DynamoDB table.
const body = JSON.parse(event.body);
const { postTitle, postBody, imgUrl, tags, id } = body
const params = {
TableName: process.env.DYNAMO_TABLE_NAME,
Key: {
id
},
ExpressionAttributeValues: {
":postTitle": postTitle,
":postBody": postBody,
":imgUrl": imgUrl,
":tags": tags
},
UpdateExpression:
"SET postTitle = :postTitle, postBody = :postBody, imgUrl = :imgUrl, tags = :tags",
ReturnValues: "ALL_NEW"
};
We are getting the data from the request payload, there is one additional property that we need to send with the request is id
of the item which we want to update.
ExpressionAttributeValues – DynamoDB has many reserved keywords so there may be a case where our table field name matches with that reserved keyword, then in that case this update will throw an error. To avoid this DynamoDB has a system of setting the original field name with some alternate name temporarily just for this purpose, so we are setting all the fields values in this object.
UpdateExpression – To update any item in DynamoDB we need to pass the field name with their respective update expression.
ReturnValues – This is just indicating that we need the updated fields data in the response when we’ll run our update operation.
const data = await dynamoDb.update(params).promise();
if (data.Attributes) {
return sendResponse(200, data.Attributes);
} else {
return sendResponse(404, { message: "Updated post data not found" });
}
Now we just need to call the update API with the params, we are also checking if updated attributes data were returned or not, if yes then we are returning that data otherwise we are returning 404 status code with a message.
Whole update.js file
"use strict";
const { sendResponse } = require("../functions/index");
const dynamoDb = require("../config/dynamoDb");
module.exports.updatePost = async event => {
try {
const body = JSON.parse(event.body);
const { postTitle, postBody, imgUrl, tags, id } = body
const params = {
TableName: process.env.DYNAMO_TABLE_NAME,
Key: {
id
},
ExpressionAttributeValues: {
":postTitle": postTitle,
":postBody": postBody,
":imgUrl": imgUrl,
":tags": tags
},
UpdateExpression:
"SET postTitle = :postTitle, postBody = :postBody, imgUrl = :imgUrl, tags = :tags",
ReturnValues: "ALL_NEW"
};
const data = await dynamoDb.update(params).promise();
if (data.Attributes) {
return sendResponse(200, data.Attributes);
} else {
return sendResponse(404, { message: "Updated post data not found" });
}
} catch (e) {
return sendResponse(500, { message: "Could not update this post" });
}
};
This lambda function will be in delete.js file, in this lambda function we are going to delete an item from the table.
"use strict";
const { sendResponse } = require("../functions/index");
const dynamoDb = require("../config/dynamoDb");
module.exports.deletePost = async event => {
try {
const body = JSON.parse(event.body);
const { id } = body;
const params = {
TableName: process.env.DYNAMO_TABLE_NAME,
Key: {
id
}
};
await dynamoDb.delete(params).promise();
return sendResponse(200, { message: "Post deleted successfully" });
} catch (e) {
return sendResponse(500, { message: "Could not delete the post" });
}
};
This lambda function is self-explanatory, we are just getting the id
of the item which we want to remove in the request and we are passing that as a param in the DynamoDB delete API.
"use strict";
const dynamoDb = require("../config/dynamoDb");
const { sendResponse } = require("../functions/index");
module.exports.listPosts = async event => {
try {
const params = {
TableName: process.env.DYNAMO_TABLE_NAME,
}
const posts = await dynamoDb.scan(params).promise();
return sendResponse(200, { items: posts.Items });
} catch (e) {
return sendResponse(500, { message: "Could not get the posts list" });
}
};
This function will be in list.js file, we are doing a very simple DynamoDB scan here and returning the data.
Check out more:
This article was first published