crud app

How to Build a CRUD Application with AWS DynamoDB, NextJS and Tailwind CSS

2024-03-31

||

10 mins read

Hey there! So you're looking to building a CRUD app using Next.js and DynamoDB? Awesome choice! This combination brings together the powerful frontend capabilities of Next.js with the scalable and flexible data storage of DynamoDB.

Next.js provides an excellent foundation for creating React-based web applications, offering features like server-side rendering, routing, and API routes. DynamoDB, on the other hand, is a fully managed NoSQL database service provided by AWS, making it easy to store and retrieve data at any scale.

In this article, you'll learn how to set up your Next.js project, integrate it with DynamoDB using the AWS SDK, and implement the CRUD operations (Create, Read, Update, Delete) to manage your data effectively.

Step 1: Creating a user in AWS IAM with permission to access DynamoDB

  1. Open the IAM Console: Once signed in, navigate to the IAM Console by selecting "Services" in the top left corner, searching for "IAM" and selecting it from the results.
aws console
  1. Navigate to Users: In the IAM Dashboard, click on "Users" in the left-hand menu to view existing users or create new ones.
iam dashboard
  1. Create a New User: Click on the "Add user" button to start creating a new user.
users page in iam
  1. Enter User Details: Enter a username for the new user.
create user page in iam
  1. Set Permissions: In the "Set permissions" step, choose attach policies directly to the user. Since we want to grant access to DynamoDB, we'll attach policy AmazonDynamoDBFullAccess directly and click Next and click Create User.
create user page in iam
  1. Create Access Key and Secret Access Key: In the Users tab, click the user that we have just created.

Click on "Create access key".

Copy the Access key and Secret access key and keep it safe and click "Done".

Step 2: Creating Table in DynamoDB

  1. Open the DynamoDb Console: Once signed in, navigate to the DynamoDb Console by selecting "Services" in the top left corner, searching for "DynamoDb" and selecting it from the results.
DynamoDb console
  1. Create a New Table: Click on the "create table" button to start creating a new table.
dynamodb console
  1. Enter Table Details: Enter Table name and Partition key and its data type and click "create table".
create table page in aws

Step 3: Creating a NextJs Project

You can create a new Next.js project using the create-next-app command. Open your terminal or command prompt and run the following command:

npx create-next-app dynamo-nextjs

Keep the default settings for the questions it asks.

Step 4: Install AWS DynamoDB sdk

Now we need to install @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb to connect the nextjs app with aws.

cd dynamo-nextjs
npm i @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

Step 5: Initial Setup

project structure

The above is the structure of the application.

Clear default code from app/page.js.

export default async function Home() {
  return (
    <div>Home</div>
  );
}

app/page.js

import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={`${inter.className} min-h-screen bg-gray-50 dark:bg-gray-900`}>{children}</body>
    </html>
  );
}

app/layout.js

create .env file to store the access key and secret access key.

NEXT_PUBLIC_AWS_ACCESS_KEY_ID=
NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY=

.env

Step 5: Configuring NextJs to Connect with DynamoDB

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

const REGION = "ap-south-1";

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,
  },
});

const marshallOptions = {
  convertEmptyValues: false, 
  removeUndefinedValues: true,
  convertClassInstanceToMap: false,
};

const unmarshallOptions = {
  wrapNumbers: false, 
};

const ddbDocClient = DynamoDBDocumentClient.from(ddbClient, {
  marshallOptions,
  unmarshallOptions,
});

export { ddbDocClient };

src/config/dbconfig.js

This code sets up a connection to DynamoDB using the AWS SDK for JavaScript. It imports necessary libraries for DynamoDB interaction and specifies the AWS region where the DynamoDB tables are located. Then, it creates a DynamoDB client with the provided region and AWS credentials obtained from environment variables.

change the REGION variable depending the region you have created the table.

Step 6: Building Pages

const [tableData, setTableData] = useState([]);

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

app/page.js

This code snippet utilizes React's useState hook to manage state, initializing tableData as an empty array and setTableData as the function to update it. The scanTable function is defined, which asynchronously scans a DynamoDB table named "Users" using the ddbDocClient (previously initialized) and retrieves all items using the ScanCommand. Upon successful retrieval, it updates the tableData state with the retrieved items. If an error occurs during the process, it logs the error to the console.

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

app/page.js

The function called deleteItem that deletes an item from a DynamoDB table named "Users". The function takes two parameters: primaryKeyValue and sortKeyValue, which represent the primary key and sort key values of the item to be deleted. Inside the function, it sends a delete command to DynamoDB, specifying the table name and the key of the item to delete. If the deletion is successful, it logs a success message to the console and then calls the scanTable function to refresh the data displayed in the application. If an error occurs during the deletion process, it logs the error to the console.

The entire code with ui of the page "app/page.js" is below

"use client";
import { useEffect, useState } from "react";
import { ddbDocClient } from "@/config/dbconfig";
import { ScanCommand } from "@aws-sdk/lib-dynamodb";
import { DeleteCommand } from "@aws-sdk/lib-dynamodb";
import Link from "next/link";

export default function Home() {
  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);
    } catch (err) {
      console.log("Error", err);
    }
  };
  const deleteItem = async (primaryKeyValue, sortKeyValue) => {
    try {
      await ddbDocClient.send(
        new DeleteCommand({
          TableName: "Users",
          Key: {
            id: primaryKeyValue,
            dateAdded: sortKeyValue,
          },
        })
      );
      console.log("Success - item deleted");
      scanTable();
    } catch (err) {
      console.log("Error", err);
    }
  };

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

  return (
    <div className="bp-3 sm:p-5 md:p-20">
      <div className="mx-auto max-w-screen-xl px-4 lg:px-12">
        <div className="bg-white dark:bg-gray-800 relative shadow-md sm:rounded-lg overflow-hidden">
          <div className="w-full md:w-auto flex flex-col md:flex-row my-2 mr-5 space-y-2 md:space-y-0 items-stretch md:items-center justify-end md:space-x-3 flex-shrink-0">
            <Link
              href={{
                pathname: "/adduser",
              }}
            >
              <button
                type="button"
                className="flex items-center justify-center text-white bg-indigo-700 hover:bg-indigo-800 focus:ring-4 focus:ring-indigo-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-indigo-600 dark:hover:bg-indigo-700 focus:outline-none dark:focus:ring-indigo-800"
              >
                Add User
              </button>
            </Link>
          </div>
          <div className="">
            <table className="w-full text-sm  text-gray-500 dark:text-gray-400">
              <thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                <tr>
                  <th scope="col" className="text-center py-3">
                    ID
                  </th>
                  <th scope="col" className="text-center py-3">
                    First Name
                  </th>
                  <th scope="col" className="text-center py-3">
                    Last Name
                  </th>
                  <th scope="col" className="text-center py-3">
                    City
                  </th>
                  <th scope="col" className="text-center py-3">
                    Phone Number
                  </th>
                  <th scope="col" className="text-center py-3">
                    Actions
                  </th>
                </tr>
              </thead>
              <tbody>
                {tableData &&
                  tableData.map((row, idx) => (
                    <tr
                      scope="row"
                      key={idx}
                      className="border-b dark:border-gray-700"
                    >
                      <td className="text-center py-3">{row.id}</td>
                      <td className="text-center py-3">{row.firstName}</td>
                      <td className="text-center py-3">{row.lastName}</td>
                      <td className="text-center py-3">{row.city}</td>
                      <td className="text-center py-3">{row.phoneNumber}</td>
                      <td className="text-center py-3">
                        <button
                          id="apple-imac-27-dropdown-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"
                          type="button"
                          onClick={() => deleteItem(row.id, row.dateAdded)}
                        >
                          Delete
                        </button>
                      </td>
                    </tr>
                  ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  );
}

app/page.js

We have to create a new page to add a data to the Users table. Paste the below code in "app/adduser/page.js".

"use client";
import { PutCommand } from "@aws-sdk/lib-dynamodb";
import { ddbDocClient } from "@/config/dbconfig";
import { useRouter } from "next/navigation";

export default function AddUser() {
  const router = useRouter();

  const handleSubmit = async (event) => {
    event.preventDefault();
    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));
      router.push("/");

      document.getElementById("addData-form").reset();
    } catch (err) {
      console.log("Error", err.stack);
    }
  };
  return (
    <div className="flex flex-col space-y-2 sm:pt-5 md:pt-20">
      <div className="text-2xl font-bold text-center mb-10 dark:text-gray-50 text-gray-900">
        ADD USER
      </div>
      <div className="w-full  ">
        <form
          onSubmit={handleSubmit}
          className="max-w-screen-sm  mx-auto"
          id="addData-form"
        >
          <div className="mb-5">
            <label
              htmlFor="firstName"
              className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
            >
              First Name
            </label>
            <input
              type="text"
              id="firstName"
              className="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 dark:shadow-sm-light"
              placeholder="Jhon"
              required
            />
          </div>

          <div className="mb-5">
            <label
              htmlFor="lastName"
              className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
            >
              Last Name
            </label>
            <input
              type="text"
              id="lastName"
              className="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 dark:shadow-sm-light"
              placeholder="Doe"
              required
            />
          </div>

          <div className="mb-5">
            <label
              htmlFor="city"
              className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
            >
              City
            </label>
            <input
              type="text"
              id="city"
              className="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 dark:shadow-sm-light"
              placeholder="Bangalore"
              required
            />
          </div>

          <div className="mb-5">
            <label
              htmlFor="phoneNumber"
              className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
            >
              Phone Number
            </label>
            <input
              type="text"
              id="phoneNumber"
              className="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 dark:shadow-sm-light"
              placeholder="+91 4506070654"
              required
            />
          </div>

          <button
            type="submit"
            className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
          >
            Add User
          </button>
        </form>
      </div>
    </div>
  );
}

app/adduser/page.js

The code creates a form which adds the data to the Users table in dynamoDb.

By following these steps, you'll have successfully built a application to handle users using Next.js, Tailwind CSS, and DynamoDB.

Use the below command to run the application in localhost.

npm run dev

You can get the source code here.