DEV Community

Cover image for Unlocking the Power of NoSQL: Building a Todo API with Hapi.js and DynamoDB
Jimmy for AWS Community Builders

Posted on • Edited on

Unlocking the Power of NoSQL: Building a Todo API with Hapi.js and DynamoDB

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 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

fullstacksaiyan; | Twitter, Instagram, Facebook, TikTok | Linktree

Share about Web, Mobile, Backend, Frontend, and Cloud Computing.

favicon linktr.ee

Hapi.js

Hapi.js 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:

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

$ aws --version
aws-cli/2.13.23 Python/3.11.5 Windows/10 exe/AMD64 prompt/off
Enter fullscreen mode Exit fullscreen mode
  • Configure AWS CLI: After installation, configure your AWS CLI,then run the following command and ensuring it returns your user information:
$ aws sts get-caller-identity
{
    "UserId": "YOUR-USER-ID",
    "Account": "YOUR-ACCOUNT-ID",
    "Arn": "arn:aws:iam::YOUR-ACCOUNT-ID:user/YOUR-USER-NAME"
}
Enter fullscreen mode Exit fullscreen mode

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

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

Table of Contents

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

Obtain Access and Secret Keys

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) :
$ 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"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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).
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:PutItem",
        "dynamodb:GetItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Scan",
        "dynamodb:Query"
      ],
      "Resource": "*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Then create the policy using the AWS CLI:

$ 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"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Attach the Policy to the User: Attach the DynamoDB CRUD policy to the IAM user you created earlier:
$ aws iam attach-user-policy --user-name your-username --policy-arn arn:aws:iam::YOUR-ACCOUNTID:policy/dynamodb-crud-policy
Enter fullscreen mode Exit fullscreen mode
  • Generate Access Keys: You can generate access and secret access keys for the IAM user:
$ 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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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

To set up our project, follow these steps:

  • Create a todoapp folder.
  • Initialize your project by running:
mkdir todoapp && cd todoapp
npm init -y
Enter fullscreen mode Exit fullscreen mode
  • Install the required packages for your project using the following command:
npm install @hapi/hapi @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb dotenv
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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.

Configure DynamoDB Client

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

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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

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:
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);
  }
})();
Enter fullscreen mode Exit fullscreen mode
  • 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.

node migrate.js
Enter fullscreen mode Exit fullscreen mode

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.

$ aws dynamodb list-tables
{
    "TableNames": [
        "your_table_name"
    ]
}
Enter fullscreen mode Exit fullscreen mode

Create CRUD Handler

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 :

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;
Enter fullscreen mode Exit fullscreen mode

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

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,
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

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

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,
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

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

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,
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

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

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,
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

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

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,
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

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

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,
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

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

module.exports = {
  addTodo,
  getTodos,
  getTodo,
  updateTodo,
  deleteTodo,
  updateCompleted,
};
Enter fullscreen mode Exit fullscreen mode

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.

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,
};
Enter fullscreen mode Exit fullscreen mode

Register Handlers to Routes

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

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

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,
  },
];
Enter fullscreen mode Exit fullscreen mode

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

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:
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);
})();
Enter fullscreen mode Exit fullscreen mode
  • 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

To test the API, you can follow these steps:

  • Run the server
node index.js
Enter fullscreen mode Exit fullscreen mode
  • Add Todo
$ 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}}
Enter fullscreen mode Exit fullscreen mode
  • Get All Todo Item
$ curl -X GET http://localhost:8080/todos -H 'Content-Type: application/json' 

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

{"title":"Build API with DynamoDB","iscompleted":false,"id":"1697484371663"}
Enter fullscreen mode Exit fullscreen mode
  • Update Todo Item status IsCompleted
$ 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}}
Enter fullscreen mode Exit fullscreen mode
  • Delete Todo Item
$ curl -X DELETE http://localhost:8080/todos/1697484371663 -H 'Content-Type: application/json'

Delete Todo Success
Enter fullscreen mode Exit fullscreen mode

Cleanup

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

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

Conclusion

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

Top comments (0)