Unlocking the Power of NoSQL: Building a Todo API with Hapi.js and DynamoDB
Tags: dynamodb, hapijs, serverless, node
Published: 2023-10-17 06:16:37

In this article, we will explore how to build a Todo API service with Hapi.js and harnessing the power of Amazon DynamoDB as a serverless NoSQL database provided by AWS

## DynamoDB

[Amazon DynamoDB](https://aws.amazon.com/dynamodb/) is a managed NoSQL database service provided by Amazon Web Services (AWS). It is designed for applications that require seamless and fast performance at any scale. DynamoDB is known for its high availability, durability, and scalability, making it suitable for a wide range of use cases, including web and mobile applications, gaming, IoT (Internet of Things), and more.

## Follow me for more
{% embed https://linktr.ee/fullstacksaiyan %}

## Hapi.js

[Hapi.js](https://hapi.dev/) commonly referred to as "hapi," is an open-source web application framework for building web and application server systems in Node.js. It was created by Walmart Labs and is designed to provide a flexible and robust foundation for building web applications, APIs, and other networked software.

## Prerequisites

Before we dive into the implementation, make sure you have the following prerequisites in place:

- **Install AWS CLI:** You can install the AWS Command Line Interface by following the [official documentation](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).

Make sure that your AWS CLI is already installed by running this command and it should return the respective output.

```sh
$ aws --version
aws-cli/2.13.23 Python/3.11.5 Windows/10 exe/AMD64 prompt/off
```

- **Configure AWS CLI:** After installation, [configure your AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html),then run the following command and ensuring it returns your user information:

```sh
$ aws sts get-caller-identity
{
    "UserId": "YOUR-USER-ID",
    "Account": "YOUR-ACCOUNT-ID",
    "Arn": "arn:aws:iam::YOUR-ACCOUNT-ID:user/YOUR-USER-NAME"
}
```

Additionally, make sure you have the `AdministratorAccess` policy attached to your AWS CLI user by running:

```sh
$ aws iam list-attached-user-policies --user-name YOUR-USERNAME
{
    "AttachedPolicies": [
        {
            "PolicyName": "AdministratorAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
        }
    ]
}
```

## Table of Contents

1. [Obtain Access and Secret Keys](#step-1)
2. [Project Preparation](#step-2)
3. [Configure DynamoDB Client](#step-3)
4. [Create DynamoDB Table](#step-4)
5. [Create CRUD Handler](#step-5)
6. [Register Handlers to Routes](#step-6)
7. [Create Server and Register Route](#step-7)
8. [Test the API](#step-8)
9. [Cleanup](#step-9)
10. [Conclusion](#step-10)

## Obtain Access and Secret Keys <a name="step-1"></a>

To obtain Access Key and Secret Key, follow these steps:

- **Create a User:** You can create a new IAM user using the AWS CLI. (Replace `YOUR-USERNAME` with the desired username) :
```sh
$ aws iam create-user --user-name YOUR-USERNAME
{
    "User": {
        "Path": "/",
        "UserName": "YOUR-USERNAME",
        "UserId": "YOUR-USERID",
        "Arn": "arn:aws:iam::YOUR-ACCOUNTID:user/YOUR-USERNAME",
        "CreateDate": "2023-10-16T17:46:59+00:00"
    }
}
```

- **Create a DynamoDB CRUD Policy:** Create a policy that defines the permissions for DynamoDB. You can use a JSON policy document like the one below and save it in a file (e.g. `dynamodb-crud-policy.json`).

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:PutItem",
        "dynamodb:GetItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Scan",
        "dynamodb:Query"
      ],
      "Resource": "*"
    }
  ]
}
```

Then create the policy using the AWS CLI:

```sh
$ aws iam create-policy --policy-name dynamodb-crud-policy --policy-document file://dynamodb-crud-policy.json
{
    "Policy": {
        "PolicyName": "dynamodb-crud-policy",
        "PolicyId": "XXXXXX",
        "Arn": "arn:aws:iam::YOUR-ACCOUNTID:policy/dynamodb-crud-policy",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2023-10-16T17:48:17+00:00",
        "UpdateDate": "2023-10-16T17:48:17+00:00"
    }
}
```

- **Attach the Policy to the User:** Attach the DynamoDB CRUD policy to the IAM user you created earlier:

```sh
$ aws iam attach-user-policy --user-name your-username --policy-arn arn:aws:iam::YOUR-ACCOUNTID:policy/dynamodb-crud-policy
```

- **Generate Access Keys:** You can generate access and secret access keys for the IAM user:

```sh
$ aws iam create-access-key --user-name your-username
{
    "AccessKey": {
        "UserName": "YOUR-USERNAME",
        "AccessKeyId": "YOUR-ACCESSID",
        "Status": "Active",
        "SecretAccessKey": "YOUR-SECRET-ACCESS-KEY",
        "CreateDate": "2023-10-16T17:49:44+00:00"
    }
}
```

This command will return the `AccessKeyId` and `SecretAccessKey` for your IAM user. Be sure to save these keys securely, as the secret access key will not be retrievable again.

Now you have created a user, attached a DynamoDB CRUD policy to that user, and generated access keys for them using the AWS CLI. Ensure you store these keys securely, as they provide access to your AWS resources.

## Project Preparation <a name="step-2"></a>

To set up our project, follow these steps:

- Create a `todoapp` folder.
- Initialize your project by running:

```sh
mkdir todoapp && cd todoapp
npm init -y
```

- Install the required packages for your project using the following command:

```sh
npm install @hapi/hapi @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb dotenv
```

- Create a `.env` file to store your environment variables, including your AWS credentials and other configuration settings. Here's an example of the content:

```
PORT=your_available_port
HOST=127.0.0.1

TABLE_NAME=your_table_name

AWS_REGION=your_aws_region
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
```

- Replace the placeholders (`your_available_port, your_table_name, your_aws_region, your_access_key_id, and your_secret_access_key`) with your actual values. The AWS credentials (`access key and secret access key`) are essential for your application to interact with DynamoDB and you can get it from [previous step](#step-1).

## Configure DynamoDB Client <a name="step-3"></a>

For DynamoDB client configuration, create a `client.js` file and include the following code:

```js
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");

exports.client = new DynamoDBClient({
  region: process.env.AWS_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
});
```

This code exports a DynamoDB client object that can be used to interact with a DynamoDB table. The `DynamoDBClient` is imported from the `@aws-sdk/client-dynamodb` package.

The `exports.client` statement exports a new instance of the `DynamoDBClient` class. The constructor for the `DynamoDBClient` class takes an object with two properties: `region` and `credentials`. The `region` property is set to the value of the `AWS_REGION` environment variable. The `credentials` property is an object with two properties: `accessKeyId` and `secretAccessKey`. These properties are set to the values of the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables, respectively.

## Create DynamoDB Table <a name="step-4"></a>

To create your DynamoDB table using the AWS SDK, follow these steps:

- Create a `migrate.js` file in your project and include the following code:

```js
require("dotenv").config();
const { CreateTableCommand } = require("@aws-sdk/client-dynamodb");
const client = require("./client").client;

(async () => {
  try {
    const command = new CreateTableCommand({
      TableName: process.env.TABLE_NAME,
      AttributeDefinitions: [
        {
          AttributeName: "id",
          AttributeType: "S",
        },
      ],
      KeySchema: [
        {
          AttributeName: "id",
          KeyType: "HASH",
        },
      ],
      BillingMode: "PAY_PER_REQUEST",
    });

    const response = await client.send(command);
    return response;
  } catch (error) {
    console.error(error.message);
  }
})();
```

- This code creates a DynamoDB table using the AWS SDK for JavaScript. It first imports the `CreateTableCommand` class from the `@aws-sdk/client-dynamodb` package and the `client` object from a local `client.js` file. The `client` object is an instance of the `DynamoDBClient` class, which is used to interact with DynamoDB.

- The code then defines a new `CreateTableCommand` object, passing in an object with several properties. The `TableName` property is set to the value of the `TABLE_NAME` environment variable. The `AttributeDefinitions` property is an array of objects that define the attributes of the table. In this case, there is only one attribute, `id`, which is a string. The `KeySchema` property is an array of objects that define the primary key of the table. In this case, the primary key is the `id` attribute. The `BillingMode` property is set to `"PAY_PER_REQUEST"`, which means that the table will be billed on a per-request basis.

- The code then sends the `CreateTableCommand` to DynamoDB using the `client.send()` method. This method returns a promise that resolves to an object containing information about the table that was created.

- If an error occurs during the execution of the code, the error message is logged to the console.

> To run the create table code in `migrate.js`, you can run this command in your terminal.

```sh
node migrate.js
```

> To check whether the table is created or not, you can run this AWS CLI command in your terminal. It should output a list of tables created in your DynamoDB.

```sh
$ aws dynamodb list-tables
{
    "TableNames": [
        "your_table_name"
    ]
}
```

## Create CRUD Handler <a name="step-5"></a>

Now, let's create the `CRUD (Create, Read, Update, Delete)` handlers for managing our todo items.

Create `handlers.js`, then start by typing this code :

```js
const client = require("./client").client;
const {
  DynamoDBDocumentClient,
  PutCommand,
  UpdateCommand,
  QueryCommand,
  DeleteCommand,
  ScanCommand,
} = require("@aws-sdk/lib-dynamodb");

const docClient = DynamoDBDocumentClient.from(client);

const TableName = process.env.TABLE_NAME;
```

This code imports the `client` object from a local `client.js` file and several classes from the `@aws-sdk/lib-dynamodb` package. The imported classes are `DynamoDBDocumentClient`, `PutCommand`, `UpdateCommand`, `QueryCommand`, `DeleteCommand`, and `ScanCommand`. These classes are used to interact with a DynamoDB table.

The `docClient` object is created by calling the `DynamoDBDocumentClient.from()` method and passing in the `client` object. This creates a new instance of the `DynamoDBDocumentClient` class that is configured to use the `client` object to interact with DynamoDB.

The `TableName` constant is set to the value of the `TABLE_NAME` environment variable.

Still in `handlers`.js file, continue including following code below.

### 1. Create Add Todo Handler

```js
const addTodo = async (request, h) => {
  const { title } = request.payload;

  try {
    const command = new PutCommand({
      TableName,
      Item: {
        id: Date.now().toString(),
        title: title,
        iscompleted: false,
      },
    });

    const response = await docClient.send(command);
    return h.response({
      status: "success",
      message: response
    }).code(201);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};
```

This code defines an asynchronous function named `addTodo` that takes a `request` object and an `h` object as arguments. The `request` object contains information about the HTTP request that was made, and the `h` object contains methods for constructing an HTTP response.

The function extracts the `title` property from the `request.payload` object, which contains the payload of the HTTP request. This property is used as the value of the `title` property in the object passed to the `PutCommand` constructor. The `id` property is set to the current timestamp converted to a string, which ensures that each todo item has a unique identifier.

The `command` object is created by calling the `PutCommand` constructor with an object that has two properties: `TableName` and `Item`. The `TableName` property is set to the value of the `TableName` constant, which is defined elsewhere in the code. The `Item` property is an object with three properties: `id`, `title`, and `iscompleted`. These properties are set to the values extracted from the `request.payload` object and the string `"false"`, respectively.

The `docClient.send()` method is called with the `command` object, which sends a request to DynamoDB to add the item to the table. If the request is successful, the function returns an HTTP response with a status code of 201 and the response from DynamoDB as the response body. If an error occurs during the execution of the code, the error message is returned as an HTTP response.

### 2. Create Get All Todo Items Handler

```js
const getTodos = async (request, h) => {
  try {
    const command = new ScanCommand({
      TableName,
    });

    const response = await docClient.send(command);
    return h
      .response({
      status: "success",
      message: response.Items
    })
      .code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};
```

This code defines an asynchronous function named `getTodos` that takes a `request` object and an `h` object as arguments. The `request` object contains information about the HTTP request that was made, and the `h` object contains methods for constructing an HTTP response.

The function creates a new `ScanCommand` object with an object that has two properties: `TableName` and `Limit`. The `TableName` property is set to the value of the `TableName` constant, which is defined elsewhere in the code. The `Limit` property is set to `1`, which means that only one item will be returned in the response.

The `docClient.send()` method is called with the `command` object, which sends a request to DynamoDB to scan the table for items. If the request is successful, the function returns an HTTP response with a status code of 200 and an object containing the items returned by the scan as the response body. If an error occurs during the execution of the code, the error message is returned as an HTTP response.

### 3. Create Get Todo Item by Id Handler

```js
const getTodo = async (request, h) => {
  const { id } = request.params;

  try {
    const command = new QueryCommand({
      TableName,
      KeyConditionExpression: "id = :id",
      ExpressionAttributeValues: {
        ":id": id,
      },
    });

    const response = (await docClient.send(command)).Items[0];
    return h.response({
      status: "success",
      message: response
    }).code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};
```

This code defines an asynchronous function named `getTodo` that takes a `request` object and an `h` object as arguments. The `request` object contains information about the HTTP request that was made, and the `h` object contains methods for constructing an HTTP response.

The function extracts the `id` property from the `request.params` object, which contains the parameters of the HTTP request. The `id` property is used as the value of the `:id` placeholder in the `KeyConditionExpression`.

The `command` object is created by calling the `QueryCommand` constructor with an object that has several properties. The `TableName` property is set to the value of the `TableName` constant, which is defined elsewhere in the code. The `KeyConditionExpression` property is a string that specifies the condition that the item must meet to be returned. In this case, the `id` attribute must be equal to the value of the `:id` placeholder. The `ExpressionAttributeValues` property is an object that maps placeholder names to the values that will be substituted for them in the `KeyConditionExpression`. In this case, the `:id` placeholder is mapped to the value extracted from the `request.params` object.

The `docClient.send()` method is called with the `command` object, which sends a request to DynamoDB to query the table for items that meet the specified condition. If the request is successful, the function returns an HTTP response with a status code of 200 and the first item that matches the query as the response body. If no items match the query, the response body will be `undefined`. If an error occurs during the execution of the code, the error message is returned as an HTTP response.

### 4. Create Update Todo Item Handler

```js
const updateTodo = async (request, h) => {
  const { id } = request.params;
  const { title } = request.payload;

  try {
    const command = new UpdateCommand({
      TableName,
      Key: {
        id: id,
      },
      UpdateExpression: "set title = :title",
      ExpressionAttributeValues: {
        ":title": title,
      },
      ReturnValues: "UPDATED_NEW",
    });

    const response = await docClient.send(command);
    return h.response({
      status: "success",
      message: response
    }).code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};
```

This code defines an asynchronous function named `updateTodo` that takes a `request` object and an `h` object as arguments. The `request` object contains information about the HTTP request that was made, and the `h` object contains methods for constructing an HTTP response.

The function extracts the `id` property from the `request.params` object, which contains the parameters of the HTTP request. The `id` property is used as the value of the `Key.id` property in the object passed to the `UpdateCommand` constructor. The `title` property is extracted from the `request.payload` object, which contains the payload of the HTTP request. This property is used as the value of the `:title` placeholder in the `UpdateExpression`.

The `command` object is created by calling the `UpdateCommand` constructor with an object that has several properties. The `TableName` property is set to the value of the `TableName` constant, which is defined elsewhere in the code. The `Key` property is an object with one property: `id`. This property is set to the value extracted from the `request.params` object. The `UpdateExpression` property is a string that specifies the update to be made to the item. In this case, the `title` attribute is set to the value of the `title` property extracted from the `request.payload` object. The `ExpressionAttributeValues` property is an object that maps placeholder names to the values that will be substituted for them in the `UpdateExpression`. In this case, the `:title` placeholder is mapped to the value of the `title` property extracted from the `request.payload` object. The `ReturnValues` property specifies which attributes of the updated item should be returned in the response.

The `docClient.send()` method is called with the `command` object, which sends a request to DynamoDB to update the item with the specified `id` value. If the request is successful, the function returns an HTTP response with a status code of 200 and the updated item as the response body. If an error occurs during the execution of the code, the error message is returned as an HTTP response.

### 5. Create Update Todo Item Status IsCompleted Handler

```js
const updateCompleted = async (request, h) => {
  const { id } = request.params;

  try {
    const command = new UpdateCommand({
      TableName,
      Key: {
        id: id,
      },
      UpdateExpression: "set iscompleted = :iscompleted",
      ExpressionAttributeValues: {
        ":iscompleted": true,
      },
      ReturnValues: "UPDATED_NEW",
    });

    const response = await docClient.send(command);
    return h.response({
      status: "success",
      message: response
    }).code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};
```

This code defines an asynchronous function named `updateCompleted` that takes a `request` object and an `h` object as arguments. The `request` object contains information about the HTTP request that was made, and the `h` object contains methods for constructing an HTTP response.

The function extracts the `id` property from the `request.params` object, which contains the parameters of the HTTP request. The `id` property is used as the value of the `Key.id` property in the object passed to the `UpdateCommand` constructor.

The `command` object is created by calling the `UpdateCommand` constructor with an object that has several properties. The `TableName` property is set to the value of the `TableName` constant, which is defined elsewhere in the code. The `Key` property is an object with one property: `id`. This property is set to the value extracted from the `request.params` object. The `UpdateExpression` property is a string that specifies the update to be made to the item. In this case, the `iscompleted` attribute is set to `true`. The `ExpressionAttributeValues` property is an object that maps placeholder names to the values that will be substituted for them in the `UpdateExpression`. In this case, the `:iscompleted` placeholder is mapped to the value `true`. The `ReturnValues` property specifies which attributes of the updated item should be returned in the response.

The `docClient.send()` method is called with the `command` object, which sends a request to DynamoDB to update the item with the specified `id` value. If the request is successful, the function returns an HTTP response with a status code of 200 and the updated item as the response body. If an error occurs during the execution of the code, the error message is returned as an HTTP response.

### 6. Create Delete Todo Item Handler

```js
const deleteTodo = async (request, h) => {
  const { id } = request.params;
  const command = new DeleteCommand({
    TableName,
    Key: {
      id: id,
    },
  });

  try {
    const response = await docClient.send(command);
    return h.response({
      status: "success",
      message: "Delete Todo Success"
    }).code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};
```

This code defines an asynchronous function named `deleteTodo` that takes a `request` object and an `h` object as arguments. The `request` object contains information about the HTTP request that was made, and the `h` object contains methods for constructing an HTTP response.

The function extracts the `id` property from the `request.params` object, which contains the parameters of the HTTP request. The `id` property is used as the value of the `Key.id` property in the object passed to the `DeleteCommand` constructor.

The `command` object is created by calling the `DeleteCommand` constructor with an object that has two properties: `TableName` and `Key`. The `TableName` property is set to the value of the `TableName` constant, which is defined elsewhere in the code. The `Key` property is an object with one property: `id`. This property is set to the value extracted from the `request.params` object.

The `docClient.send()` method is called with the `command` object, which sends a request to DynamoDB to delete the item with the specified `id` value. If the request is successful, the function returns an HTTP response with a status code of 200 and a message indicating that the todo has been successfully deleted. If an error occurs during the execution of the code, the error message is returned as an HTTP response.

### 7. Export all the functions

```js
module.exports = {
  addTodo,
  getTodos,
  getTodo,
  updateTodo,
  deleteTodo,
  updateCompleted,
};
```

This code exports an object with several functions that can be used to handle HTTP requests. The functions are named `addTodo`, `getTodos`, `getTodo`, `updateTodo`, `deleteTodo`, and `updateCompleted`.

> **The complete code of `handlers.js` will look like this.**

```js
const client = require("./client").client;
const {
  DynamoDBDocumentClient,
  PutCommand,
  UpdateCommand,
  QueryCommand,
  DeleteCommand,
  ScanCommand,
} = require("@aws-sdk/lib-dynamodb");

const docClient = DynamoDBDocumentClient.from(client);

const TableName = process.env.TABLE_NAME;

const addTodo = async (request, h) => {
  const { title } = request.payload;

  try {
    const command = new PutCommand({
      TableName,
      Item: {
        id: Date.now().toString(),
        title: title,
        iscompleted: false,
      },
    });

    const response = await docClient.send(command);
    return h.response({
      status: "success",
      message: response
    }).code(201);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};

const getTodos = async (request, h) => {
  try {
    const command = new ScanCommand({
      TableName,
    });

    const response = await docClient.send(command);
    return h
      .response({
      status: "success",
      message: response
    })
      .code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};

const getTodo = async (request, h) => {
  const { id } = request.params;

  try {
    const command = new QueryCommand({
      TableName,
      KeyConditionExpression: "id = :id",
      ExpressionAttributeValues: {
        ":id": id,
      },
    });

    const response = (await docClient.send(command)).Items[0];
    return h.response({
      status: "success",
      message: response
    }).code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};

const updateTodo = async (request, h) => {
  const { id } = request.params;
  const { title } = request.payload;

  try {
    const command = new UpdateCommand({
      TableName,
      Key: {
        id: id,
      },
      UpdateExpression: "set title = :title",
      ExpressionAttributeValues: {
        ":title": title,
      },
      ReturnValues: "UPDATED_NEW",
    });

    const response = await docClient.send(command);
    return h.response({
      status: "success",
      message: response
    }).code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};

const updateCompleted = async (request, h) => {
  const { id } = request.params;

  try {
    const command = new UpdateCommand({
      TableName,
      Key: {
        id: id,
      },
      UpdateExpression: "set iscompleted = :iscompleted",
      ExpressionAttributeValues: {
        ":iscompleted": true,
      },
      ReturnValues: "UPDATED_NEW",
    });

    const response = await docClient.send(command);
    return h.response({
      status: "success",
      message: response
    }).code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};

const deleteTodo = async (request, h) => {
  const { id } = request.params;
  const command = new DeleteCommand({
    TableName,
    Key: {
      id: id,
    },
  });

  try {
    const response = await docClient.send(command);
    return h.response({
      status: "success",
      message: "Delete Todo Success"
    }).code(200);
  } catch (error) {
    return h.response({
      message: error.message,
    });
  }
};

module.exports = {
  addTodo,
  getTodos,
  getTodo,
  updateTodo,
  deleteTodo,
  updateCompleted,
};
```

## Register Handlers to Routes <a name="step-6"></a>

Now, let's register the handlers to Hapi.js routes.

Create a `routes.js` file and include the following code:

```js
const todoHandler = require("./handlers");

exports.routes = [
  {
    method: "POST",
    path: "/todos",
    handler: todoHandler.addTodo,
  },
  {
    method: "GET",
    path: "/todos",
    handler: todoHandler.getTodos,
  },
  {
    method: "GET",
    path: "/todos/{id}",
    handler: todoHandler.getTodo,
  },
  {
    method: "PUT",
    path: "/todos/{id}",
    handler: todoHandler.updateTodo,
  },
  {
    method: "DELETE",
    path: "/todos/{id}",
    handler: todoHandler.deleteTodo,
  },
  {
    method: "PUT",
    path: "/todos/{id}/completed",
    handler: todoHandler.updateCompleted,
  },
];
```

This codes exports an array of objects that define the routes for a RESTful API. Each object in the array represents a single route and contains the following properties:

- `method`: The HTTP method for the route (e.g. GET, POST, PUT, DELETE).
- `path`: The URL path for the route.
- `handler`: The function that will handle the request for the route.

The `handler` property is set to a function that is imported from a module named `todoHandler`.

## Create Server and Register Route <a name="step-7"></a>

To create the Hapi.js server and register the routes, follow these steps:

- Create an index.js file in your project and include the following code:

```js
require("dotenv").config();
const Hapi = require("@hapi/hapi");

const { routes } = require("./routes");

(async () => {
  const server = Hapi.server({
    port: process.env.PORT,
    host: process.env.HOST,
  });

  server.route(routes);

  await server.start();
  console.log("Server running on %s", server.info.uri);
})();
```

- This code sets up a Hapi server that listens for incoming HTTP requests. It first loads environment variables from a `.env` file using the `dotenv` package. The `Hapi` package is then imported, which provides a framework for building HTTP servers in Node.js.

- The `routes` object is imported from a separate file named `routes.js`. This object contains the routes that the server will handle.

- An asynchronous function is defined using an immediately invoked function expression (IIFE) that creates a new instance of the `Hapi.server` class. The `port` and `host` properties of the server are set to the values of the `PORT` and `HOST` environment variables, respectively.

- The `server.route()` method is called with the `routes` object, which sets up the routes that the server will handle.

- The `server.start()` method is called to start the server. If the server starts successfully, a message is logged to the console indicating the URL that the server is listening on.

## Test the API <a name="step-8"></a>
To test the API, you can follow these steps:
- Run the server
```sh
node index.js
```
- Add Todo
```sh
$ curl -X POST http://localhost:8080/todos -H 'Content-Type: application/json' -d '{"title":"Build API with DynamoDB"}'

{"$metadata":{"httpStatusCode":200,"requestId":"cb51867a-2e1e-4d62-8ce6-f709825d2505","attempts":1,"totalRetryDelay":0}}
```
- Get All Todo Item
```sh
$ curl -X GET http://localhost:8080/todos -H 'Content-Type: application/json' 

{"response":[{"title":"Build API with DynamoDB","iscompleted":false,"id":"1697484371663"}]}
```
- Get Todo Item by Id
```sh
$ curl -X GET http://localhost:8080/todos/1697484371663 -H 'Content-Type: application/json'

{"title":"Build API with DynamoDB","iscompleted":false,"id":"1697484371663"}
```
- Update Todo Item status IsCompleted
```sh
$ curl -X PUT http://localhost:8080/todos/1697484371663/completed -H 'Content-Type: application/json'

{"$metadata":{"httpStatusCode":200,"requestId":"b13129b1-1087-4e07-b205-60e067938327","attempts":1,"totalRetryDelay":0},"Attributes":{"iscompleted":true}}
```
- Delete Todo Item
```sh
$ curl -X DELETE http://localhost:8080/todos/1697484371663 -H 'Content-Type: application/json'

Delete Todo Success
```

## Cleanup <a name="step-9"></a>

For cleanup, we can delete our DynamoDB table that we created before by typing this command in our terminal.

```sh
$ aws dynamodb delete-table --table-name your_table_name
{
    "TableDescription": {
        "AttributeDefinitions": [
            {
                "AttributeName": "id",
                "AttributeType": "S"
            }
        ],
        "TableName": "your_table_name",
        "KeySchema": [
            {
                "AttributeName": "id",
                "KeyType": "HASH"
            }
        ],
```



## Conclusion <a name="step-10"></a>

In this blog post, we learn about building a Todo API using the powerful Hapi.js framework and harnessing the capabilities of Amazon DynamoDB. Throughout this tutorial, we've seen how these two technologies can be seamlessly integrated to create a robust and scalable solution for managing tasks and to-do lists.

Hapi.js, with its ease of use and extensive ecosystem of plugins, provided us with a solid foundation to build our API. Its routing, validation, and error-handling capabilities make it an excellent choice for developing RESTful services. Moreover, we appreciated the emphasis on configuration-driven development, which simplifies the setup process and enhances maintainability.

DynamoDB, Amazon's fully managed NoSQL database, proved to be a valuable asset in our project. Its scalability, high availability, and serverless architecture ensured that our Todo API can handle both small-scale tasks and grow seamlessly to accommodate larger workloads. The performance and durability of DynamoDB, combined with features like automatic backups and global tables, give us peace of mind when it comes to data integrity.

In conclusion, building a Todo API with Hapi.js and DynamoDB is a rewarding experience that equips you with the tools to create powerful, scalable, and resilient applications.

## Check out my previous post

{% embed https://dev.to/aws-builders/generate-image-from-text-using-amazon-bedrocks-stable-diffusion-xl-model-via-aws-cli-5112 %}