Published on

Let's build a book app API with harperdb

Authors

Let's build a book app API with harperdb and fastify. It will be a simple CRUD API that exposes endpoints to insert, view, update and delete a book. We will be using harperdb as our database of choice because of how easy it is to get up and running with it no matter what type of application you are building, and how fast its query operations is, whether SQL or NoSQL. We will also use fastify as the nodejs web framework for our routing.

What you will need

Before we get started, I assume you are familiar with javascript, nodejs and have a basic understanding of Restful API. Below are some other requirements which are not technical:

  • A code editor (Like Vscode).
  • API client (Like Postman or insomnia).
  • Free tier Harperdb cloud instance

Set up HarperDB

To sign up for a harperdb account, visit harperdb sign up page and fill the registration form. You can use any of these coupon code "HARPER4FREE" or "MargoHDB" to get $250 free credit.

After successful registration, you will be redirected to the instances page as shown on the image below:

Harperdb Instance page

On a new account, you currently have no cloud instance, and you will be required to create one to be able to proceed with this tutorial.

Click on the plus (+) icon on the Create New HarperDB Cloud Instance card to open a instance creation dialog modal with two options like on the image below:

Harperdb Instance page

Out of the two options on the dialog modal, click on the create harperDB cloud instance on the left to bring up the new instance info form. This form is to collect the details of the cloud instance you want to create.

Harperdb Instance page

Complete the form and take note of the cloud name, instance url, username, and password of the new instance you want to create and click on instance details to proceed to the next screen where you will select the configuration of your new instance. These data will be needed in your app to configure the connection to your cloud instance.

Harperdb Instance page

For this tutorial, we will be using the free tier cloud instance, hence leave the instance configuration at it default and click on confirm instance details to proceed to confirm instance details screen.

Harperdb Instance page

If you are satisfied with your instance details which you should at this point, choose i agree and click on add instance to create your new instance.

You will be prompted to wait while your instance is being provisioned and after it has been successfully provisioned, you will see a new instance card on the instance page/dashboard with details of the created instance

Harperdb Instance page

Click on the newly created instance card to open the instance page to start defining your schema and tables that will host or store your data. A schema is like a database or a collection of tables.

Harperdb Instance page

To proceed, you will be required to create a schema. Enter the name of your schema and click on the check button to save it. You can name your schema anything you want. I called my schema tutorials.

Harperdb Instance page

After that, you will be prompted to create a table to store our book record, so you can call your table books and as for the hash attribute which is more or less like a unique or primary key/identifier, enter id and click on the check button to create the book table.

Harperdb Instance page

Congratulations! you have completed harperdb setup and table creation and are ready to start developing the book app API

Server set up with fastify

First of all, create a new directory/folder and navigate into it by running the following command in your command line.

//this will create a new directory in your current directory
mkdir harperdb-books

//change your current directory to your project directory
cd harperdb-books

To initialize node package manager (npm), install fastify and its dependencies run the command below inside your current project directory cli


//This command will initialize npm and bootstrap fastify setup
npm init fastify

// This will install fastify and all its dependencies
npm install


//This command will start the server in watch mode at the default port of 3000
npm run dev

Running server page

At this point, your fastify server is up and running on http://localhost:3000 and port 3000 is the default port for fastify from the bootstrapped setup. If you try to access http://localhost:3000/example on your API client, you will get this response this is an example

Running server page

You can change this port to any other port by creating a .env file in your project root directory and setting the port to any port number like in the example below. This functionality comes out of the box with fastify init bootstrap setup.

//.env
PORT = 5000

You will not need to restart your server when you change your port since the server is running on the watch mode (Server will restart when a file change is detected). The changed port will display on your command line, and if for any reason the old port still display, kindly press control+c on your keyboard to stop the server and then re-run your server by running npm run dev in the command line interface.

Connecting to HarperDB cloud instance.

Before we start creating endpoints and sending request with data to the database, we will need to connect our app to the HarperDB cloud instance we created. To achieve this, we will install Harperive (a node.js driver for HarperDb). Run the command below in your project root directory to install harperive.

npm install harperive --save

When the installation is done open your .env file and enter the following information. Replace the instance details with the details of the instance you created in the upper section of the tutorials.

//.env
INSTANCE_URL=https://cloud-1-royallink.harperdbcloud.com //replace this with your cloud instance url
INSTANCE_USERNAME=myharpercloud //replace this with your cloud instance username
INSTANCE_PASSWORD=password //replace this with your cloud instance password
INSTANCE_SCHEMA=tutorials //replace this with your schema name

Now create a directory/folder in your project root directory and name it config and then create a new file inside the config directory and call it dbconfig.js. Inside the dbconfig.js file, add the following code

//.dbclient.js
const harperive = require('harperive')

const DB_CONFIG = {
  harperHost: process.env.INSTANCE_URL,
  username: process.env.INSTANCE_USERNAME,
  password: process.env.INSTANCE_PASSWORD,
  schema: process.env.INSTANCE_SCHEMA, // optional params
}

const Client = harperive.Client
const client = new Client(DB_CONFIG)

module.exports = client

In the code snippet above, we imported the harperive we installed and created a DB_CONFIG object to hold the value of the host which is the cloud instance URL, username, password, and schema we saved in our .env file. We then instantiated a harperive client and pass in the DB_CONFIG object as a parameter. Then we exported the client so that we can import it anywhere we need to connect to our Harperdb cloud instance.

Now the book CRUD endpoint

We will start working on the CRUD endpoint now. Create a new directory inside the routes directory and call it api, and then create another directory inside the api directory and call it books. Then create an index.js file inside the books directory and add the following code.

// routes/api/books/index.js

//import the harperdb client
const client = require('../../../config/dbconfig')

//input validation schema
const bookOptions = {
  schema: {
    body: {
      type: 'object',
      required: ['title', 'price', 'publisher', 'author'],
      properties: {
        title: { type: 'string' },
        price: { type: 'number' },
        publisher: { type: 'string' },
        author: { type: 'string' },
      },
    },
  },
}

module.exports = async function (fastify, opts) {
  // crud endpoints goes here......
}

In the code above, we first imported the harperive client we exported earlier, this will enable us to establish a connection to our cloud instance through harperive.

The next line of code is a variable of JSON schema that will help with basic input validation. We provided the required input data and their datatype which we will pass to the add new book endpoint as an argument to help validate the request body content, to prevent sending empty data to the db.

Next we created and exported an async function as a module to house all our CRUD endpoints. The function accepts a fastify object that contains the http methods for restful services and options argument. Now lets create our first endpoint to add a book.

// routes/api/books/index.js

module.exports = async function (fastify, opts) {
  //add a new book endpoint
  fastify.post('/', bookOptions, async (request, reply) => {
    try {
      const res = await client.insert({
        table: 'books',
        records: [request.body],
      })
      reply.code(201).send(res.data)
    } catch (err) {
      console.log(err)
    }
  })
}

Our first endpoint above is a post method which we will use to create or add a new book to our cloud instance (db). It accepts three argument, the request path, the JSON schema we created earlier, and an async callback function with request and reply parameters.

The request parameter contain a body property which is the data that is sent from the API client like Postman or the frontend.

We can now use the imported harperdb client to access some of the harperDB queries like insert query the is used to insert a record or multiple records to our db. See list of queries you can access from harperive

The insert method accepts an object with the name of the table to insert the data to and records which is an array of objects to insert in the database table. In our case our record is just a JSON object sent from our API client in the request body.

Before you test our add new book endpoint, our code should look like this:

// routes/api/books/index.js

//import the harperdb client
const client = require('../../../config/dbconfig')

//input validation schema
const bookOptions = {
  schema: {
    body: {
      type: 'object',
      required: ['title', 'price', 'publisher', 'author'],
      properties: {
        title: { type: 'string' },
        price: { type: 'number' },
        publisher: { type: 'string' },
        author: { type: 'string' },
      },
    },
  },
}

module.exports = async function (fastify, opts) {
  //add a new book endpoint
  fastify.post('/', bookOptions, async (request, reply) => {
    try {
      const res = await client.insert({
        table: 'books',
        records: [request.body],
      })
      reply.code(201).send(res.data)
    } catch (err) {
      console.log(err)
    }
  })
}

If you code looks exactly like the one above, open your API client lets test it. On an empty tab or a new one enter this url http://localhost:5000/api/books, and choose POST as your request type, click on the body tab and select raw and the past the code below:

{
  "title":"Fifth Avenue Style",
  "price":"60.54",
  "publisher": "Vendome Press; Illustrated edition",
  "author":"Howard Slatkin"
}

Your Api client should look like the one on the image below:

Running server page

Make sure your server is still running and if it is not type npm run dev on your terminal to restart the server, the go back to your api client and click on send to send the post request.

If your request and insert operations is successful, you will get a response in your API client similar to the one below and also on your HarperDB studio cloud instance table, you will see a new record inserted with the same data.

If you have any error, kindly confirm your cloud instance details on your dbclient.db file and make sure they are correct.

Running server page
Running server page

As you can see from the image above, HarperDB automatically created three new fields and added their data along with the ones we sent, They are id field, createdtime field, and updatedtime field.

Try to add more data from the ones provided below to allow us perform operations.

//data 2
{
  "title":"The Innovator's Dilemma",
  "price":"15.87",
  "publisher": "Harvard Business School Press",
  "author":"Clayton M. Christensen"
}

//data 3
{
  "title":"The Effective Engineer",
  "price":"29.35",
  "publisher": "Effective Bookshelf",
  "author":"Edmond Lau"
}

//data 4
{
  "title":"Obsidian",
  "price":"5.9",
  "publisher": "Hodder & Stoughton",
  "author":"Jennifer L. Armentrout"
}

//data 5
{
  "title":"How To Win Friends and Influence People",
  "price":"60.54",
  "publisher": "HarperTorch",
  "author":"Dale Carnegie"
}

Your table should now look like the one below if you have successfully added the provided data above.

Running server page

Get all books

Now that we have successfully added some book records in our cloud instance table, lets see how we can query and get all the books we inserted.

Right below our add book endpoint, add the following code

//get all book endpoint
fastify.get('/', async function (request, reply) {
  const querySelect = 'select * from tutorials.books'
  try {
    const res = await client.query(querySelect)
    reply.status(200).send(res.data)
  } catch (err) {
    console.log(err)
  }
})

This is a get request endpoint to fetch all the books in the database. Here you will notice that we are writing an SQL query to be able to achieve this. At the moment, there is no function in harperive just like the insert function we used above to insert a record to the database, which we can use to perform fetch operations. But we can an SQL query just like in any SQL database to query and get data from the database. All we have to do is to pass a valid select query or statement to the harperive client query and it will do the magic for us.

Now your full code should look like this:

// routes/api/books/index.js

//import the harperdb client
const client = require('../../../config/dbconfig')

//input validation schema
const bookOptions = {
  schema: {
    body: {
      type: 'object',
      required: ['title', 'price', 'publisher', 'author'],
      properties: {
        title: { type: 'string' },
        price: { type: 'number' },
        publisher: { type: 'string' },
        author: { type: 'string' },
      },
    },
  },
}

module.exports = async function (fastify, opts) {
  //add a new book endpoint
  fastify.post('/', bookOptions, async (request, reply) => {
    try {
      const res = await client.insert({
        table: 'books',
        records: [request.body],
      })
      reply.code(201).send(res.data)
    } catch (err) {
      console.log(err)
    }
  })

  //get all book endpoint
  fastify.get('/', async function (request, reply) {
    const querySelect = 'select * from tutorials.books'
    try {
      const res = await client.query(querySelect)
      reply.status(200).send(res.data)
    } catch (err) {
      console.log(err)
    }
  })
}

Now lets try to access the endpoint from our API client and see what will be returned. Open a new tab on your API client and enter http://localhost:5000/api/books. Make sure you choose GET as your request type and click send.

If your request is successful, you will get all the books in the database returned in the response as shown below

get all book endpoint

Get a single book by hash (id)

To get a single book by hash, add the code below underneath the get all books endpoint

fastify.get('/:id', async function (request, reply) {
  try {
    const res = await client.searchByHash({
      table: 'books',
      hashValues: [request.params.id],
      attributes: ['*'],
    })
    reply.code(200).send(res.data)
  } catch (err) {
    console.log(err)
    return fastify.httpErrors.internalServerError('Something went wrong')
  }
})

In the code above, the endpoint accepts an id param which will be sent as an hash value. Remember when we created a table in our cloud instance, we were asked to enter a hash value that will serve as a unique identifier. This unique identifier is used to query the database.

Harperive has a method that we can used to query and get books by their hashes, searchByHash. In the method, you provide an object with the following properties: table name, hashValues and attributes. For the hashValue property, you pass in an array of hash values and the books with those hash values will be returned. So if you provide one hash id, an array of one book object will be returned. Also, you can specify the attribute or the fields you want to be returned. This is very useful when you only want the endpoint to return some fields and not all. In our case we want all the fields hence the Asterisk.

Now your complete code should look like this:

// routes/api/books/index.js

//import the harperdb client
const client = require('../../../config/dbconfig')

//input validation schema
const bookOptions = {
  schema: {
    body: {
      type: 'object',
      required: ['title', 'price', 'publisher', 'author'],
      properties: {
        title: { type: 'string' },
        price: { type: 'number' },
        publisher: { type: 'string' },
        author: { type: 'string' },
      },
    },
  },
}

module.exports = async function (fastify, opts) {
  //add a new book endpoint
  fastify.post('/', bookOptions, async (request, reply) => {
    try {
      const res = await client.insert({
        table: 'books',
        records: [request.body],
      })
      reply.code(201).send(res.data)
    } catch (err) {
      console.log(err)
    }
  })

  //get all book endpoint
  fastify.get('/', async function (request, reply) {
    const querySelect = 'select * from tutorials.books'
    try {
      const res = await client.query(querySelect)
      reply.status(200).send(res.data)
    } catch (err) {
      console.log(err)
    }
  })

  fastify.get('/:id', async function (request, reply) {
    try {
      const res = await client.searchByHash({
        table: 'books',
        hashValues: [request.params.id],
        attributes: ['*'],
      })
      reply.code(200).send(res.data)
    } catch (err) {
      console.log(err)
      return fastify.httpErrors.internalServerError('Something went wrong')
    }
  })
}

Now go back to your API client and access this get endpoint http://localhost:5000/api/books/:id. Replace the :id with an actual id of one of your books in the database. You should get a response like the one below.

Get a single book by hash

Update a single book by hash (id)

Lets try to update a single book. This method can also be used to update multiple book instances. Add the code below

fastify.patch('/', async function (request, reply) {
  try {
    const res = await client.update({
      table: 'books',
      records: [request.body],
    })
    reply.code(201).send(res.data)
  } catch (err) {
    console.log(err)
    return fastify.httpErrors.internalServerError('Something went wrong')
  }
})

Here, the harperive client update method requires an object of two properties, the table name and the records you want to update, You pass in a single book object with the properties and the values of the books you intend to update. In the case where you want to update multiple items, you will need multiple book objects as items in the records array.

Note: You books Json object must contain the hash value. As you can see, there is no id or hash params in the endpoint. So you will have to provide the hash in the request body object. Lets assume you want to update the price, the author and the title, take a look at the example below:

{
    "price": 20,
    "author": "Clayton Christensen",
    "title": "The Innovator Dilemma",
    "id": "054acd12-6322-41ec-adc9-59a029b6728e" // Replace this with the actual id or Hash value of the book you want to update
}

With the above example, send a patch request to this update endpoint http://localhost:5000/api/books in your API client. Make sure you send it as a Json object just like we did when we added a book. Your response should look similar to the one on the image below:

Update a single book by hash

Delete a single book by hash (id)

To delete a book we use a harperive client delete method and pass in the table name and the hash values of the books you want to delete in the client method param object.

fastify.delete('/:id', async function (request, reply) {
  const options = {
    table: 'books',
    hashValues: [request.params.id],
  }
  try {
    const res = await client.delete(options)
    reply.code(200).send(res.data)
  } catch (err) {
    console.log(err)
    return fastify.httpErrors.internalServerError('Something went wrong')
  }
})

As you can see in the code snippet above, we accepted the id of the book we want to delete in the request param, and pass it as an array item to the harperive client delete method params object hash value property.

Now in your API client, send a delete request to the endpoint http://localhost:5000/api/books/:id.

Delete a single book by hash

Search for a book by value

HarperDB provides a way to search for data in the database, and you can do that using Harperive also. To do that, you have to select the fields you want to search as the search attribute, the search term as the search value and select the fields you want returned in the attributes or you use the wildcat selector to select all fields.

fastify.get('/search', async function (request, reply) {
  const options = {
    table: 'books',
    searchAttribute: 'title',
    searchValue: request.query.title,
    attributes: ['*'],
  }
  try {
    const res = await client.searchByValue(options)
    reply.code(200).send(res.data)
  } catch (err) {
    console.log(err)
    return fastify.httpErrors.internalServerError('Something went wrong')
  }
})

Here we want to search for books by their title, lets assume we have more than one book with the same title. We sent a get request to this endpoint http://localhost:5000/api/books/search?title=searchTerm. And pass in title as the query string and the query string value as The Innovator Dilemma. The query string value, is then accessed and sent as the search value. Open the endpoint http://localhost:5000/api/books/search?title=The Innovator Dilemma. with your API client.

Note: You will have to provide the search term in full for this to work. for example if the book title is The Innovator Dilemma and you provide The Innovator, you will get an empty array as a response.

Search for a book by value

Conclusion

As you can see, HarperDB is very easy to use. It supports NoSQL and SQL queries. Although most of the operations we did were NoSQL Query operations, SQL Query is also as easy. Take a look at the get all books operation we did to confirm that. You can also read more on how to use more of SQL Query at the harperive website.

You can access the full source code on github