Building a Next.js DynamoDB CRUD App

Sankha Rathnayake
Level Up Coding
Published in
12 min readJan 3, 2023

--

Background Photo by Alejandro Ortiz on Unsplash

Next.js is an open-source javascript framework that facilitates to build React based applications with server-side rendering. DynamoDB is a NoSQL database service that is provided by AWS. In this article, we’ll be talking about how we can build a simple CRUD app using these two.

So let’s get things started. In case you already have an IAM user account, you can skip reading the below topic. But you need to make sure that you have already added ‘AmazonDynamoDBFullAccess” policy to the permissions of the IAM user that you are currently using.

Creating a user in AWS IAM with permission to access DynamoDB

  1. First, you need to log into your AWS account as a root user. If you don’t have an AWS account, follow this guide to create an account.
  2. Search for ‘IAM’ in the top search bar in Console Home. Click on IAM from the search results.

3. Click on ‘Users’ in Left Pane. Thereafter, click on the ‘Add Users’ button

4. Give a user name and the access type as ‘Programmatic Access’. After that, Click on ‘Next: Permissions’

5. Click on ‘Attach existing policies directly’ and then type ‘dynamo’ in the search bar. Check ‘AmazonDynamoDBFullAccess” policy.

7. Click “Next: Tags”. In tags section, you can add details (Email address, Job title etc.) related to the user as key-value pairs. After adding tags, Click ‘Next: Review’.

8. Review the configurations. Mine looks like below.

9. Once you are done with reviewing, Click ‘Create User’.

10. Copy the Access key ID and Secret access key value and paste it to a safe location.

11. To communicate with your AWS account via the terminal of your computer, you need to install AWS CLI on your computer. Follow this guide to get it done.

Configuring your computer with AWS Credentials

  1. After installing AWS CLI in your machine, you need to open a new terminal and type aws configure to set up your machine with access to your AWS account.
  2. The below guide is taken from AWS docs. You can follow that to complete the configuration.

Setting Up Next.js Project

Now let’s get into the section that we truly love. That is, CODING!!

  1. Open the terminal and run npx create-next-app@latest command and provide answers to the questions as in the below screenshot.

2. Install below AWS SDK dependencies. These libraries will contain the functions that will help us to do CRUD operations

npm i @aws-sdk/client-dynamodb

npm i @aws-sdk/lib-dynamodb

3. We’ll be using tailwind css for styling. So please follow this guide to configure it for your project. Tailwind CSS is a framework that is based on atomic css concept

4. Now let’s store the AWS credentials in the project. Create a file named .env (place it along with package.json), and mention below two lines in that file.

NEXT_PUBLIC_AWS_ACCESS_KEY_ID="Your-access-key"
NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY="Your-secret-key"

5. Assign your AWS user’s access key into NEXT_PUBLIC_AWS_ACCESS_KEY_ID and secret key into NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY. It’s important to have NEXT_PUBLIC_ part at the beginning of each environment variable to make it accessible from the js files that we’ll create later in the project.
(Please note that exposing AWS credentials to the browser (client-side) like this is not recommended for production-grade websites. I have done that here only to simplify the process)

6. Create a folder named ‘config’ at the root of the project (along with package.json file). Create a file named dbconfig.js inside that folder and paste below code into that file.

// Create service client module using ES6 syntax.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";

// Set the AWS Region.
const REGION = "us-east-1"; //e.g. "us-east-1"
// Create an Amazon DynamoDB service client object.

const ddbClient = new DynamoDBClient({
region: REGION,
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY,
},
});

export { ddbClient };

This dbconfig.js file will create a dynmodbclient with our credentials. So that we can use this client to access our dynamodb in AWS and do the needed operations (Create table, CRUD operations etc.)

7. Create a file named ‘ddbDocClient.js’ inside the same folder and paste below code into that file.

  • This file will configure the marshalling and unmarshalling options.
  • Here marshalling means converting the javascript values to DynamoDB attribute values based on a defined schema. unmarshalling means doing it the other way around.

// Create a service client module using ES6 syntax.
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { ddbClient } from "./dbconfig.js";

const marshallOptions = {
// Whether to automatically convert empty strings, blobs, and sets to `null`.
convertEmptyValues: false, // false, by default.
// Whether to remove undefined values while marshalling.
removeUndefinedValues: true, // false, by default.
// Whether to convert typeof object to map attribute.
convertClassInstanceToMap: false, // false, by default.
};

const unmarshallOptions = {
// Whether to return numbers as a string instead of converting them to native JavaScript numbers.
wrapNumbers: false, // false, by default.
};

// Create the DynamoDB document client.
const ddbDocClient = DynamoDBDocumentClient.from(ddbClient, {
marshallOptions,
unmarshallOptions,
});

export { ddbDocClient };

Creating a table programmatically in DynamoDB

Create a file named ‘createTable.js’ in ‘pages’ folder and paste the below code into that file.

// Import required AWS SDK clients and commands for Node.js
import { CreateTableCommand } from "@aws-sdk/client-dynamodb";
import { ddbClient } from "../config/dbconfig";

// Set the parameters
export const params = {
// Add the partionkey and sort key(if needed) together with their types
AttributeDefinitions: [
{
AttributeName: "id", //Primary Key name
AttributeType: "N", //Type of the primary key
},
{
AttributeName: "dateAdded", //Sort key name
AttributeType: "S", //Type of the sort key
},
],
// Declaring which one is primary key and which one is sort key out of above defined attributes.
// For Primary key -> KeyType = HASH
// For Sort key -> KeyType = RANGE
KeySchema: [
{
AttributeName: "id", //Primary key name
KeyType: "HASH",
},
{
AttributeName: "dateAdded", //Sort key name
KeyType: "RANGE",
},
],
ProvisionedThroughput: {
ReadCapacityUnits: 5,
WriteCapacityUnits: 5,
},
TableName: "Users", //TABLE_NAME
StreamSpecification: {
StreamEnabled: true,
StreamViewType: "KEYS_ONLY",
},
};

const CreateTable = () => {
const run = async () => {
try {
const data = await ddbClient.send(new CreateTableCommand(params));
console.log("Table Created", data);
alert("Table Created!")
return data;
} catch (err) {
console.log(err);
}
};

return (
<div className="flex flex-col justify-center items-center h-screen">
<button
className="px-6
py-2.5
bg-blue-600
text-white
font-medium
text-xs
leading-tight
uppercase
rounded
shadow-md
hover:bg-blue-700 hover:shadow-lg
focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg
transition
duration-150
ease-in-out"
onClick={() => run()}
>
Create Table
</button>
</div>
);
};

export default CreateTable;
  • There are two modes to process reads and writes capacity on DynamoDB tables. They are On-demand and Provisioned. So I will be using Provisioned mode which is eligible for free-tier. So in provisioned mode, we need to specify the number of reads and writes per second that we need for our application. That’s what we have done using ReadCapacityUnits and WriteCapacityUnits To learn more about both of these modes, please refer to this guide.
  • DynamoDB Streams capture the changes that have been done to the items in the tables. It maintains this information in a log for up to 24 hours. We have enabled this feature by configuring StreamSpecification section in the above code. You can learn more about this by following this guide

Inserting Data into DynamoDB table

Create a file named ‘adddata.js’ in ‘pages’ folder and paste the below code into that file.

In this small project, I will be creating a simple user data browser. Each item (record) in ‘Users’ table will have these attributes - id, dateAdded, dateModified, firstName, lastName, city, phoneNumber,

import { PutCommand } from "@aws-sdk/lib-dynamodb";
import { ddbDocClient } from "../config/ddbDocClient";
import { useRouter } from "next/router";

const styles = {
inputField:
"form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none",
};

const AddData = () => {
const router = useRouter();

const handleSubmit = async (event) => {
// Stop the form from submitting and refreshing the page.
event.preventDefault();

// Get data from the form.
const params = {
TableName: "Users",
Item: {
id: Math.floor(Math.random() * 10000),
dateAdded: new Date().toLocaleString(),
dateModified: "",
firstName: event.target.firstName.value,
lastName: event.target.lastName.value,
city: event.target.city.value,
phoneNumber: event.target.phoneNumber.value,
},
};

try {
const data = await ddbDocClient.send(new PutCommand(params));
console.log("Success - item added", data);
alert("Data Added Successfully");
router.push("/viewdata");
document.getElementById("addData-form").reset();
} catch (err) {
console.log("Error", err.stack);
}
};
return (
<>
<div className="flex flex-col justify-center items-center h-screen">
<p className="text-3xl mb-20">Add Data</p>
<div className="block p-6 rounded-lg shadow-lg bg-white w-1/3 justify-self-center">
<form onSubmit={handleSubmit} id="addData-form">
<div className="form-group mb-6">
<label
htmlFor="firstName"
className="form-label inline-block mb-2 text-gray-700"
>
First Name
</label>
<input type="text" className={styles.inputField} id="firstName" />
</div>
<div className="form-group mb-6">
<label
htmlFor="lastName"
className="form-label inline-block mb-2 text-gray-700"
>
Last Name
</label>
<input type="text" className={styles.inputField} id="lastName" />
</div>
<div className="form-group mb-6">
<label
htmlFor="exampleInputEmail1"
className="form-label inline-block mb-2 text-gray-700"
>
City
</label>
<input type="text" className={styles.inputField} id="city" />
</div>
<div className="form-group mb-6">
<label
htmlFor="phoneNumber"
className="form-label inline-block mb-2 text-gray-700"
>
Phone Number
</label>
<input
type="number"
className={styles.inputField}
id="phoneNumber"
/>
</div>

<button
type="submit"
className="
px-6
py-2.5
bg-blue-600
text-white
font-medium
text-xs
leading-tight
uppercase
rounded
shadow-md
hover:bg-blue-700 hover:shadow-lg
focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg
transition
duration-150
ease-in-out"
>
Submit
</button>
</form>
</div>
</div>
</>
);
};

export default AddData;

Reading and deleting data from DynamoDB table

Create a file named ‘viewdata.js’ in ‘pages’ folder and paste the below code into that file.

// Import required AWS SDK clients and commands for Node.js.
import { useEffect, useState } from "react";
import { ddbDocClient } from "../config/ddbDocClient.js";
import { ScanCommand } from "@aws-sdk/lib-dynamodb";
import { DeleteCommand } from "@aws-sdk/lib-dynamodb";
import Link from "next/link.js";

const Styles = {
tableHeadings:
"text-sm font-medium text-gray-900 px-6 py-4 text-left border-2",
tableData: "text-sm text-gray-900 font-light px-6 py-4 whitespace-nowrap",
};

const ViewData = () => {
let data = [];
const [tableData, setTableData] = useState([]);

// scanning the dynamodb table
const scanTable = async () => {
try {
data = await ddbDocClient.send(new ScanCommand({ TableName: "Users" }));
setTableData(data.Items);
console.log("success", data.Items);
} catch (err) {
console.log("Error", err);
}
};

// deleting an item from the table
const deleteItem = async (primaryKeyValue, sortKeyValue) => {
try {
await ddbDocClient.send(
new DeleteCommand({
TableName: "Users",
Key: {
id: primaryKeyValue, // primarykeyName : primaryKeyValue
dateAdded: sortKeyValue, // sortkeyName : sortkeyValue
},
})
);
console.log("Success - item deleted");
scanTable();
} catch (err) {
console.log("Error", err);
}
};

useEffect(() => {
scanTable();
}, []);

return (
<div className="container mx-auto py-10 flex flex-col w-screen h-screen items-center">
<div className="flex w-2/3 justify-end py-4">
<Link
href={{
pathname: "/adddata",
}}
>
<button
type="button"
className="inline-block px-6 py-2.5 mr-2 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"
>
Add Data
</button>
</Link>
</div>
<p className="text-3xl">View Data</p>
<div className="flex flex-col w-2/3 py-10">
<div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
<div className="overflow-hidden">
<table className="min-w-full">
<thead className="border-b">
<tr>
<th scope="col" className={Styles.tableHeadings}>
id
</th>
<th scope="col" className={Styles.tableHeadings}>
First Name
</th>
<th scope="col" className={Styles.tableHeadings}>
Last Name
</th>
<th scope="col" className={Styles.tableHeadings}>
City
</th>
<th scope="col" className={Styles.tableHeadings}>
Phone Number
</th>
<th
scope="col"
className="text-sm font-medium text-gray-900 px-6 py-4 text-center border-2"
>
Action
</th>
</tr>
</thead>
<tbody>
{tableData.map((item) => (
<tr className="border-b" key={item.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{item.id}
</td>
<td className={Styles.tableData}>{item.firstName}</td>
<td className={Styles.tableData}>{item.lastName}</td>
<td className={Styles.tableData}>{item.city}</td>
<td className={Styles.tableData}>{item.phoneNumber}</td>
<td className="text-sm text-gray-900 font-light px-6 py-4 whitespace-nowrap text-center">
<Link
href={{
pathname: "/updatedata",
query: {
id: item.id,
dateAdded: item.dateAdded,
firstName: item.firstName,
lastName: item.lastName,
city: item.city,
phoneNumber: item.phoneNumber,
},
}}
>
<button
type="button"
className="inline-block px-6 py-2.5 mr-2 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"
>
Edit
</button>
</Link>
<button
type="button"
className="inline-block px-6 py-2.5 bg-red-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-red-700 hover:shadow-lg focus:bg-red-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-red-800 active:shadow-lg transition duration-150 ease-in-out"
onClick={() => deleteItem(item.id, item.dateAdded)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
};

export default ViewData;

Updating Data in DynamoDB table

Create a file named ‘updatedata.js’ in ‘pages’ folder and paste the below code into that file.

import { ddbDocClient } from "../config/ddbDocClient";
import { useRouter } from "next/router";
import { UpdateCommand } from "@aws-sdk/lib-dynamodb";

const styles = {
inputField: "form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
}


const UpdateData = () => {
const router = useRouter();
const data = router.query;

const handleSubmit = async (event) => {
// Stop the form from submitting and refreshing the page.
event.preventDefault();

// setting up the parameters for UpdateCommand
const params = {
TableName: "Users",
Key: {
id: Number(data.id), //primaryKey
dateAdded: data.dateAdded, //sortKey
},
UpdateExpression:
"set firstName = :p, lastName = :r, city = :q, phoneNumber = :z, dateModified = :k",
ExpressionAttributeValues: {
":p": event.target.firstName.value,
":r": event.target.lastName.value,
":q": event.target.city.value,
":z": event.target.phoneNumber.value,
":k": new Date().toLocaleString(),
},
};

// updating the db
try {
const data = await ddbDocClient.send(new UpdateCommand(params));
console.log("Success - updated", data);
alert('Data Updated Successfully')
router.push('/viewdata')
} catch (err) {
console.log("Error", err);
}
};

return (
<>
<div className="flex flex-col justify-center items-center h-screen">
<p className="text-3xl mb-20">Update Data</p>
<div className="block p-6 rounded-lg shadow-lg bg-white w-1/3 justify-self-center">
<form onSubmit={handleSubmit} id="addData-form">
<div className="form-group mb-6">
<label
htmlFor="firstName"
className="form-label inline-block mb-2 text-gray-700"
>
First Name
</label>
<input
type="text"
className={styles.inputField}
id="firstName"
name="firstName"
defaultValue={data.firstName}
/>
</div>
<div className="form-group mb-6">
<label
htmlFor="lastName"
className="form-label inline-block mb-2 text-gray-700"
>
Last Name
</label>
<input
type="text"
className={styles.inputField}
id="lastName"
name="lastName"
defaultValue={data.lastName}
/>
</div>
<div className="form-group mb-6">
<label
htmlFor="exampleInputEmail1"
className="form-label inline-block mb-2 text-gray-700"
>
City
</label>
<input
type="text"
className={styles.inputField}
id="city"
name="city"
defaultValue={data.city}
/>
</div>
<div className="form-group mb-6">
<label
htmlFor="phoneNumber"
className="form-label inline-block mb-2 text-gray-700"
>
Phone Number
</label>
<input
type="number"
className={styles.inputField}
id="phoneNumber"
name="phoneNumber"
defaultValue={data.phoneNumber}
/>
</div>

<button
type="submit"
className="
px-6
py-2.5
bg-blue-600
text-white
font-medium
text-xs
leading-tight
uppercase
rounded
shadow-md
hover:bg-blue-700 hover:shadow-lg
focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg
transition
duration-150
ease-in-out"
>
Submit
</button>
</form>
</div>
</div>
</>
);
};

export default UpdateData;

References

Github Repo for this tutorial — https://github.com/sankharr/aws_crud

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job

--

--