AWS Certified Developer - Associate
Databases
DynamoDB SDK Part2 Demo
In this lesson, we demonstrate how to build a real-world CRUD application using Express, DynamoDB, and the DynamoDB SDK. Although we use Express to build Node.js APIs, you do not need an in-depth mastery of Express for the exam. This project serves as a practical example that integrates various DynamoDB operations.
The application provides endpoints to:
- Retrieve all products
- Create a new product
- Retrieve a single product by its ID
- Delete a product
- Update a product
Each endpoint in the Express application corresponds to one of these operations. For instance, a GET request to /products
returns all products stored in the DynamoDB table, while a POST request to /products
creates a new product. Other endpoints handle retrieval by ID, deletion, and updates.
Below is an initial Express setup with placeholders for each operation:
import express from "express";
import { v4 as uuidv4 } from "uuid";
const app = express();
app.use(express.json());
app.get("/products", async (req, res) => {});
app.post("/products", async (req, res) => {});
app.get("/products/:id", async (req, res) => {});
app.delete("/products/:id", async (req, res) => {});
app.put("/products/:id", async (req, res) => {});
const PORT = 3000;
app.listen(PORT, () => console.log(`app is listening on port ${PORT}`));
These five endpoints will eventually handle listing, creating, retrieving, deleting, and updating products in your DynamoDB table. Note that the table uses "id" as the partition key, so retrieving items using a scan is required since a query necessitates a specific partition key.
Tip
A scan operation retrieves all items from the table. However, it is less efficient than a query and consumes more read capacity.
Let's integrate a DynamoDB scan into our GET /products
endpoint. In the snippet below, we import the necessary DynamoDB classes and demonstrate a simple scan operation:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
export const main = async () => {
const command = new ScanCommand({
ProjectionExpression: "#Name, Color, AvgLifeSpan",
ExpressionAttributeNames: { "#Name": "Name" },
TableName: "Birds",
});
const response = await docClient.send(command);
for (const bird of response.Items) {
console.log(`${bird.Name} - ${bird.Color}, ${bird.AvgLifeSpan}`);
}
return response;
};
Notice that we import and initialize the DynamoDB client along with the document client. In our Express application, we wire up all required libraries as shown below:
import express from "express";
import { v4 as uuidv4 } from "uuid";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({/* optionally include region or credentials */});
const docClient = DynamoDBDocumentClient.from(client);
const app = express();
app.use(express.json());
app.get("/products", async (req, res) => {});
app.post("/products", async (req, res) => {});
app.get("/products/:id", async (req, res) => {});
app.delete("/products/:id", async (req, res) => {});
app.put("/products/:id", async (req, res) => {});
const PORT = 3000;
app.listen(PORT, () => console.log(`app is listening on port ${PORT}`));
Retrieving All Products
Now, let’s add logic to the GET /products
endpoint. We perform a scan on the "products" table and then return the resulting items as JSON:
const client = new DynamoDBClient({
credentials: {
// your credentials here
},
});
const docClient = DynamoDBDocumentClient.from(client);
app.get("/products", async (req, res) => {
const command = new ScanCommand({ TableName: "products" });
const response = await docClient.send(command);
console.log(response);
res.json({ items: response.Items });
});
After starting your application (e.g., via npm start
with nodemon), testing this endpoint with an API tester (like Postman) will return all products stored in your DynamoDB table. The response from DynamoDB includes metadata and primarily lists items under the property Items
.
An example output might look like:
[
{
"id": 1,
"price": 5,
"name": "soap"
},
{
"onSale": true,
"inventory": 25,
"category": "electronics",
"id": "1003",
"price": 800,
"name": "camera"
},
{
"onSale": false,
"inventory": 10,
"category": "electronics",
"id": "20",
"price": 200,
"name": "tv"
},
{
"category": "electronics",
"inventory": 5,
"id": "12",
"price": 1000,
"name": "phone"
}
]
Creating a New Product
To allow users to create a product, we handle a POST request to /products
. The application extracts product data from the request body, generates a unique ID using UUID, and uses DynamoDB’s PutCommand
to add the new item.
Remember
Ensure you import PutCommand
from @aws-sdk/lib-dynamodb
before using it.
import { PutCommand } from "@aws-sdk/lib-dynamodb";
app.post("/products", async (req, res) => {
const { body } = req;
const command = new PutCommand({
TableName: "products",
Item: {
...body,
id: uuidv4(),
},
});
const response = await docClient.send(command);
res.status(201).json({ message: "Product created successfully" });
});
You can test this endpoint using Postman or another API testing tool by sending a POST request to http://localhost:3000/products
with a JSON body like:
{
"name": "water bottle",
"price": 20,
"inventory": 10,
"category": "everyday"
}
A successful request returns a 201 status code and adds the new product to your DynamoDB table.
Retrieving a Single Product
To fetch a product by its ID, we use the GET /products/:id
endpoint. The following code extracts the id
from the URL, then utilizes the GetCommand
to retrieve the product from DynamoDB:
import { GetCommand } from "@aws-sdk/lib-dynamodb";
app.get("/products/:id", async (req, res) => {
const { id } = req.params;
const command = new GetCommand({
TableName: "products",
Key: { id: id },
});
const response = await docClient.send(command);
res.json({ item: response.Item });
});
When you test this endpoint with a valid product ID, it returns the product details. An example response could be:
{
"item": {
"onSale": false,
"inventory": 4,
"category": "electronics",
"id": "80",
"price": 2000,
"name": "laptop"
}
}
Deleting a Product
The DELETE /products/:id
endpoint handles product deletion. It extracts the product ID from the request URL and uses the DeleteCommand
to remove the corresponding item from the table:
import { DeleteCommand } from "@aws-sdk/lib-dynamodb";
app.delete("/products/:id", async (req, res) => {
const { id } = req.params;
const command = new DeleteCommand({
TableName: "products",
Key: { id: id },
});
const response = await docClient.send(command);
console.log(response);
res.status(204).json({ message: "Product deleted successfully" });
});
Sending a DELETE request with a product ID (for example, "80") returns a 204 status code, indicating that the deletion was successful.
Updating a Product
To update an existing product, use the PUT /products/:id
endpoint. In this example, we overwrite the existing item using the PutCommand
with new values provided in the request body. (Alternatively, you could use DynamoDB’s UpdateCommand
for partial updates.)
app.put("/products/:id", async (req, res) => {
const { id } = req.params;
const body = req.body;
console.log(body);
const command = new PutCommand({
TableName: "products",
Item: {
...body,
id: id,
},
});
const response = await docClient.send(command);
console.log(response);
res.status(200).json({ message: "Product updated successfully" });
});
For example, to update a product with the ID "1003" (a camera) by changing its price from 800 to 1000, you would send a PUT request with the following JSON body:
{
"name": "camera",
"price": 1000,
"onSale": true,
"category": "electronics"
}
After a successful update, retrieving the product from DynamoDB will show the updated price.
Querying Products by Category
If users want to filter products by category, you have two primary options. Although scanning with client-side filtering is feasible, using a Global Secondary Index (GSI) is more efficient. In this example, we assume that a GSI named "category-index" exists with "category" as the partition key.
Modify the GET /products
endpoint to check for a query parameter and execute a query if a category is provided:
import { QueryCommand } from "@aws-sdk/lib-dynamodb";
app.get("/products", async (req, res) => {
const { category } = req.query;
let command;
if (category) {
command = new QueryCommand({
TableName: "products",
IndexName: "category-index",
KeyConditionExpression: "category = :category",
ExpressionAttributeValues: {
":category": category,
},
});
} else {
command = new ScanCommand({ TableName: "products" });
}
const response = await docClient.send(command);
console.log(response);
res.json({ items: response.Items });
});
When called without a query parameter, the endpoint returns all products. With a query parameter (e.g., ?category=electronics
), it efficiently returns only the products within that category using the GSI.
This demonstration illustrates how to integrate various DynamoDB operations—scans, queries with a Global Secondary Index, and basic CRUD operations—within a Node.js Express application. By following these examples, you can seamlessly work with DynamoDB operations in your own projects.
For more information, consider exploring additional resources:
Watch Video
Watch video content