Node-HarperDB REST API
Tags: node, database
Published: 2020-10-27 15:44:39

# TLDR

[Completed code repo using `"books"` instead of `"games"`](https://github.com/manavm1990/harper-node-demo)

# Foreword

This post is not just for 'quick immediate gratification.' Depending on your experience level, some of this code may be a bit overwhelming, as I am trying to illustrate some 'DRY' code patterns and modular architecture along the way. Have a look 👀 at the repo 👆🏽 to get some idea 💡 before proceeding, if you like (but don't copy/paste - that's no way to learn 🙅🏽‍♂️)

1. This article is _inspired_ by this post: {% link https://dev.to/amanhimself/build-a-rest-api-with-node-js-and-harperdb-3738 %}
2. {%user margo_hdb %} herself from HarperDB covers a lot of background regarding Harper and Node here: {% link https://dev.to/amanhimself/build-a-rest-api-with-node-js-and-harperdb-3738 %}
3. In order to 'code-along,' all the way through, you may want to check this: {% link https://dev.to/codefinity/node-noob-starter-repo-g31 %}[ This 'starter template repo'](https://github.com/manavm1990/node-starter) - which the full code repo is based on 👆🏽 - includes a `webpack` configuration to allow for `import` (instead of `require`), _absolute imports,_ linting, some basic starter 🌱 architecture 🏗️, and some other goodies 🧁.

In addition to the 'Node-Harper' stuff, we'll be using '.env', a _closure_ pattern, Postman and modern EcmaScript code patterns and syntax.

The rest of this post will assume that you are using [the aforementioned 👆🏽 starter template repo, "node-starter."](https://github.com/manavm1990/node-starter), or something similar.

# Overview

We'll be creating a RESTful API written in Node with Express. We'll implement basic CRUD. I'll be making a small catalog of video games, but you can apply whatever you want.

*The assumption here is that you do have the [foundations of Node](https://nodejs.dev/learn) down and understand [the basics of what a RESTful API is](https://restfulapi.net/).*

I will be using `npm` to install additional dependencies as we move forward.

# Getting Started

Head on over to [HarperDB](https://harperdb.io/) and 'Get Started.'

Once that's done, you can create a new 'instance.' I'll call mine 'video-games,' but you can do 📕s, or 'videos' or whatever.

![Creating an HarperDB Instance called 'video games'](https://dev-to-uploads.s3.amazonaws.com/i/0d21hp5idjp3wsuzbcj9.gif)

*Take note 🎵 of your 'username' and 'password.'*

On the next screen, 'Instance Specs,' all the defaults are fine - you might pick an 'Instance Region' that's geographically closer to you.

It will take a few minutes to create the instance - that part _might_ be a bit slower than MongoDB ⏳.

Once that's done, click on that instance, and we'll need to create a simple _schema._ This is nothing but a system to describe your data.

I'll just create one called 'dev' and have a _table_ (like a spreadsheet that keeps some data) called 'games' that will keep track of them with an 'id'.'

![Create a 'dev' schema with a 'games' table with 'id](https://dev-to-uploads.s3.amazonaws.com/i/j67z6y2vu7t00fjjs0xd.gif)

## HarperDB Secret Connection Deets in '.env'

In your project directory, create a '.env' file to hold our connection details. 

This file will be ignored 🙈 via '.gitignore' so it doesn't show up in our repo (it's not there in the 'starter repo' that I mentioned 👆🏽).

From VS Code (or whatever text editor), you can paste the necessary details as shown below, replacing the relevant ones with your own:

```bash
# This will be used by express
PORT=8000

INSTANCE_URL=https://node-demo-codefinity.harperdbcloud.com
INSTANCE_USERNAME=codefinity
INSTANCE_PASSWORD=someSecretSpit

# What you entered as your schema name
INSTANCE_SCHEMA=dev
```

You can find your `INSTANCE_URL` under 'config' tab: ![Get config details](https://dev-to-uploads.s3.amazonaws.com/i/0oib3suuxysjirftixhm.gif)

## Express Server

ExpressJS is a middleware framework that sits on top of Node and does great work managing our routing _requests._ Let's install it: `npm i express`.

We'll also want: `npm i dotenv` so we can get those '.env' deets over to our `express`.

Inside of 'src/index.js,' set up a basic Express server like so:

```javascript
// 'import' works b/c of the webapack config 🤓
import express from "express";

// We are destructuring 'config' directly from 'dotenv
import { config } from "dotenv";

// Intialize stuff from '.env'
config();

const app = express();

// Configure 'Express' to receive URL encoded JSON
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// Set up a test route
app.get("/", (_, res) => {
  res.end("Express Server");
});

// Start listening for requests on our 'secret port'
app.listen(process.env.PORT, () => {
  console.log("Server 🏃🏽‍♂️", process.env.PORT);
});
```

## Starting the Express Server and Testing the 'Test Route'

The 'starter repo' that I mentioned previously 👆🏽 uses `webpack` and `nodemon` to build and 'watch' changes to our project.

In one terminal window, from your project directory: `npm run watch`. And, from a separate terminal window: `npm run serve`.

![Watch and Serve](https://dev-to-uploads.s3.amazonaws.com/i/cpp19fyy2ai4yj0d667f.gif) 

## Postman (Or Insomnia) to Test Routes

Now, you'll need [Postman](https://www.postman.com/downloads/) (or Insomnia). 

I will use Postman to test our simple get route where we should get back a _response:_ `"Express Server"`.

1. On the left, click 'New Collection.'
2. Give it a name and click the 🍊 button.
3. Click the '...' and select, 'Add Request'
4. Our local web server is running at, "http://localhost:8000." Enter that into the big field that looks like a browser bar with the word, 'GET' next to it.
5. Click the big blue 'Send' button to send that _request_ off and you should see, `Express Server` in the big space there on the right.

![Test 'GET' from Postman](https://dev-to-uploads.s3.amazonaws.com/i/qji169y8rmx2xd0ucehq.gif)

# Node-Harper Client Connection

We'll need a 'connector' that uses our Node code and our connection deets from '.env' to communicate with our HarperDB instance. We'll use: `npm i harperive`.

Now, inside of 'db/client.js,' we'll create a 'client connection' to our instance.

```javascript
import harperive from "harperive";

import { config } from "dotenv";

config();

// Pass in our deets to initialize and export the 'client'
export default new harperive.Client({
  harperHost: process.env.INSTANCE_URL,
  username: process.env.INSTANCE_USERNAME,
  password: process.env.INSTANCE_PASSWORD,

  // We only have 1 schema so we can set that directly
  schema: process.env.INSTANCE_SCHEMA,
});
```

## CRUD Operation Controllers

'db/index.js' will contain the business logic controllers that directly manipulate our database. This will be used later on by our 'express api' that will call upon the correct controller based on the incoming request - sort of the essence of 'basic' RESTful API.

We'll go over the necessary code one hunk at a time, and I'll walk you through my reasoning about why I set up the code this way.

We'll start with a 'closure callback' pattern that each of our CRUD _controllers_ will need. This is because every time we perform one of these operations, we will need to handle an error, or a successful response that will come from Harper.

### A Bit Complex...But Can DRY Your Code

```javascript
const callback = (func) => (err, res) => {
  if (err) {
    func(err);
  } else {
    func(null, res);
  }
};
```

For every CRUD operation that we will create, `add`, `search` and `delete`, no matter what, we will want to run a _callback function_ in response to either an error - `err` or a successful _response_ from HarperDB - `res`.

`(func) => (err, res) => {` Allows us to create a _callback_ that will use whatever _function_ we want it to use.

`if (err)` will pass the error to our _function_, and `else` will send the 'error' as `null` so we can proceed with doing something with our 'successful response' - `res`.

_This will make even more sense when we go to actually use it._

Continuing with 'db/index.js':

### Using `client` 🤗

```javascript
import client from "./client";

// TODO: 'const callback' code block goes here.

// Export out all of our 'controller methods'
export default {
  /**
    * Insert 1 game at a time only (for simplicity)
    * title - the game title as a string
    * platform - the game platform as a string
    * cb - the function that will handle error/success
    */
  add(title, platform, cb) {
    // TODO: 'insert' a new video game
  },
  
  // 'searchParams' is an Object with 'search parameters.'
  search(searchParams, cb) {
    // TODO: Search using either a hash/id or a value.
  },

  // 'id' is a string 
  delete(id, cb) {
    // TODO: Seek and destroy 🎸 using the given 'id'
  },
};
```

### Controller Method Guts

Next, let's insert the 'guts' of each 'controller' method, replacing the `TODO`s 👆🏽, starting with _`add`_:

```javascript
client.insert(
      { 
        // We'll hardcode this b/c we only have 1
        table: "games",
        
        // 'records' MUST be an Array (even if only 1)        
        records: [{ 
          
          // Object shorthand technique
          title, platform }] },
        
        // What to do when done?
        callback(cb)
    );
```

_Note 🎵 that: `callback(cb)` 👆🏽 is where we save some duplicate code by using `const callback` closure from earlier 👆🏽._

Next, we have, _`search`_. This one is a bit more complex only b/c we have to code this up so that it can handle using `searchByHash` if we pass in an `id` 🔑, or `searchByValues` if it's a `game` or `platform` 🔑.

```javascript
// Check for an 'id' via OBJECT DESTRUCTURING
const { id } = searchParams; 
    if (id) {
      client.searchByHash(
        {
          table: "games",
          hashValues:
            // ⚠️ MUST be wrapped in an ARRAY
            [id],

          // Only send back 'title'
          attributes: ["title"],
        },
        callback(cb)
      );
    } 

    // We must be searching by something other than 'id'
    else {

      // Use array destructuring to pull out our 🔑 and value
      const [searchParamsEntry] = Object.entries(searchParams);
      client.searchByValue(
        {
          table: "games",

          // This is the 🔑 - 'title' or 'platform'
          searchAttribute: searchParamsEntry[0],
          searchValue: searchParamsEntry[1],

          // Send back all of the details
          attributes: ["*"],
        },
        callback(cb)
      );
    }
```

Finally, we have `delete` (a bit simpler 😌):

```javascript
client.delete(
      {
        table: "games",

        // Again, the 'id' must be wrapped in an Array
        hashValues: [id],
      },
      callback(cb)
    );
```

## Express Router

Now that the _controllers_ are out of the way 💦, we can ease into creating our _api routes_ so we can test things.

```javascript
import { Router } from "express";

// Controller methods
import db from "db";

// Express router
const router = new Router();

// POST method to '/games/add/'
router.post("/add", (

// Destructure 'title' and 'platform' from request
{ body: { title, platform } }, res, next) => {
  db.add(title, platform, 

  // Callback handler
  (err, dbRes) => {
    if (err) {

      // 'Standard' Express 'built-in' error handling
      next(
        new Error(`
      ❗Error adding ➕
      ${err.error}
      `)
      );
    }
    res.status(201);
    res.json(dbRes);
  });
});

// POST method to '/games/search'
router.post("/search", (
// Destructure 'body' from request
{ body }, res, next) => {
  db.search(body, (err, dbRes) => {
    if (err) {
      next(
        new Error(`
      ❗Error searching 🔍
      ${err.error}
      `)
      );
    }
    res.status(200);
    res.json(dbRes);
  });
});

// 'DELETE' method to '/games'
router.delete("/", ({ body: { id } }, res, next) => {
  db.delete(id, (err, dbRes) => {
    if (err) {
      next(
        new Error(`
      ❗Error deleting 🔥
      ${err.error}
      `)
      );
    }
    res.status(204);
    res.json(dbRes);
  });
});

// Send the 'router' back out for the server to use
export default router;
```

Let's use `router` in our server. *Here's what the file should look 👀 like with the starting code along with adding `router` (I removed 🔥 the original 'test `get`' code):*

```javascript
import express from "express";
import { config } from "dotenv";

// Router
import api from "api";

config();

const app = express();

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// Trigger our Express JSON Router if URL ends with '/games'
app.use("/games", api);

app.listen(process.env.PORT, () => {
  console.log("Server 🏃🏽‍♂️", process.env.PORT);
});
```

## Test the Each and Every Thing 🇮🇳

Assuming that your server is still 🏃🏽‍♂️, we can finish up by testing our routes in Postman.

### 'http://localhost:8000/games/add' - `insert`

1. Switch 'GET' to 'POST.'
2. Click 'Body.'
3. Select 'raw.'
4. On the right there, select JSON.
5. Enter in valid JSON.
6. Click 'Send.'

![insert route post](https://dev-to-uploads.s3.amazonaws.com/i/vk2v1rrbowp6zri2021x.gif)

And, on the Harper side, click 'browse' to see the results.

![Browse data in Harper](https://dev-to-uploads.s3.amazonaws.com/i/29cfnhty1pxgjr5h9847.gif)

### 'http://localhost:8000/games/search' 

First, let's test out `searchByHash`.

Grab the 'id' from HarperDB. Just click on the entries and then copy it from the next screen. 

![Copy id from HarperDB](https://dev-to-uploads.s3.amazonaws.com/i/lyv3abh0gty2g63sh6t0.gif)

Following the same steps in Postman as before, create a 'POST' request and send over some JSON using the id you copied from HarperDB. For example,

```json
{
    "id": "47ecf929-405b-49d6-bd41-91e6b2c5ab48"
}
```

![Search by Id](https://dev-to-uploads.s3.amazonaws.com/i/2pgfjxrpdrnryuyxxs9l.gif)

# Finishing Up

🆗 I'll leave it to you to test out the other 'POST' search, sending in JSON with either `"title"` or `"platform"`.

For the 'DELETE,' recall that our `router` was set up to take 'DELETE' _requests_ like so:  `router.delete("/"`

You will create a 'DELETE' request and send it to: 'http://localhost:8000/games,' once again, using an `"id"`.

---

I am trying to get [my YouTube](https://www.youtube.com/channel/UCwGoouzZ4fo9AZDcvfi8LWg?view_as=subscriber) restarted; the problem is that I kind of hate making videos (especially editing)! 😃 

Would you like a video version of this tutorial? 💦 If there's enough response, maybe I will grin 😀 and 🐻 it.