Creating an application with Koa and RethinkDB

Published Sep 20, 2017
Creating an application with Koa and RethinkDB

Introduction

One of key reasons you want to write an app with Koa probably is that you want to ditch callbacks in your code. Middleware cascading in Koa is good example how it allows customised middleware to execute one after another without any notorious callbacks in JavaScript.

Screen-Shot-2014-04-11-at-7.49.09-AM.png

When a middleware invokes next() the function suspends and passes control to the next middleware defined. After there are no more middleware to execute downstream, the stack will unwind and each middleware is resumed to perform its upstream behaviour. (Quoted from http://koajs.com/)

RethinkDB is an open source JSON database built for realtime Web. It pushes JSON to your apps in realtime from the database each time a change occurs.

B_8ExX3VAAAMDtG.png

RethinkDB is the first open-source, scalable JSON database built from the ground up for the realtime web. It inverts the traditional database architecture by exposing an exciting new access model – instead of polling for changes, the developer can tell RethinkDB to continuously push updated query results to apps in realtime. (Quoted from https://www.rethinkdb.com/faq/)

Despite the fact that changefeeds lies at the heart of RethinkDB’s real-time functionality, you can skip this functionality if you want to. You can use RethinkDB just like MongoDB to store and query your database. This is what this article intended to show you how to build a RESTful CRUD app with Koa and RethinkDB.

Getting started

After having RethinkDB server installed, you will need its client drivers. To install the driver with npm for Nodejs:

$ npm install rethinkdb

Or you can include it in as one of the dependecies in the package.json. I have this setup in mine:

  "dependencies": {
    "babel-cli": "^6.24.1",
    "babel-preset-es2015": "^6.24.1",
    "cross-env": "^5.0.1",
    "koa": "^2.3.0",
    "koa-bodyparser": "^4.2.0",
    "koa-favicon": "^2.0.0",
    "koa-static": "^4.0.1",
    "koa-trie-router": "^2.1.5",
    "rethinkdb": "^2.3.3"
  },
  "scripts": {
    "dev": "cross-env NODE_PATH=./server nodemon --exec babel-node --presets es2015 server/index.js",
    "start": "cross-env NODE_ENV=production NODE_PATH=./server nodemon --exec babel-node --presets es2015 server/index.js"
  },

For the sake of simplicity, we keep all major code in server/index.js. We use babel-node --presets es2015 so that we can write ES6 code. I installed nodemon globally to help me to restart the app each time when a change occurs during the development.

To start the app, simply type this in the terminal:

$ npm start

Now we can start using rethinkdb client driver in our Koa app:

import Koa from 'koa'
import Router from 'koa-trie-router'
import r from'rethinkdb'

We will create these 5 main functions, getUsers, getUser, insertUser, insertUser, updateUser, deleteUser, and pass them in the router:

const router = new Router()
router
  .get('/users', getUsers)
  .get('/users/:name', getUser)
  .post('/users', insertUser)
  .put('/users', updateUser)
  .del('/users', deleteUser)

app.use(router.middleware())

Before that, we will need to create a function to establish the connection with the RethinkDB database:

const db = async() => {
  const connection = await r.connect({
    host: 'localhost',
    port: '28015',
    db: 'mydb'
  })
  return connection;
}

The problem to work with RethinkDB currently is that it lacks of a friendly administration tool. The web interface that comes by default when you install RethinkDB is quite basic and difficult to use. There are third-party administration tools, Chateau is the easiest among many in my view.

Developing our methods

Once you have created a new database, e.g. mydb, create a new table called users in it, then we can roll for HTTP methods below:

1. The GET method

Let's create getUsers method to query the list of users in mydb:

const getUsers = async(ctx, next) => {
  await next()

  // Get the db connection.
  const connection = await db()

  // Check if a table exists.
  var exists = await r.tableList().contains('users').run(connection)
  if (exists === false) {
    ctx.throw(500, 'users table does not exist')
  }

  // Retrieve documents.
  var cursor = await r.table('users')
    .run(connection)

  var users = await cursor.toArray()

  ctx.type = 'json'
  ctx.body = users
}

To query a single user, let's create getUser method:

const getUser = async(ctx, next) => {
  await next()
  let name = ctx.params.name

  // Throw the error if no name.
  if (name === undefined) {
    ctx.throw(400, 'name is required')
  }

  // Get the db connection.
  const connection = await db()

  // Throw the error if the table does not exist.
  var exists = await r.tableList().contains('users').run(connection)
  if (exists === false) {
    ctx.throw(500, 'users table does not exist')
  }

  let searchQuery = {
    name: name
  }

  // Retrieve documents by filter.
  var user = await r.table('users')
    .filter(searchQuery)
    .nth(0) // query for a stream/array element by its position
    .default(null) // will return null if no user found.
    .run(connection)

  // Throw the error if no user found.
  if (user === null) {
    ctx.throw(400, 'no user found')
  }

  ctx.body = user
}

When you visit the app at http://127.0.0.1:3000/users, you get:

{"status":200,"data":[]}

Note that the data is empty - "data":[], this is because there is no user added to the users table yet.

2. The POST method

To add new users to users table in mydb database, we need to create insertUser method:

const insertUser = async(ctx, next) => {
  await next()

  // Get the db connection.
  const connection = await db()

  // Throw the error if the table does not exist.
  var exists = await r.tableList().contains('users').run(connection)
  if (exists === false) {
    ctx.throw(500, 'users table does not exist')
  }

  let body = ctx.request.body || {}

  // Throw the error if no name.
  if (body.name === undefined) {
    ctx.throw(400, 'name is required')
  }

  // Throw the error if no email.
  if (body.email === undefined) {
    ctx.throw(400, 'email is required')
  }

  let document = {
    name: body.name,
    email: body.email
  }

  var result = await r.table('users')
    .insert(document, {returnChanges: true})
    .run(connection)

  ctx.body = result
}

Now if you go to Google Postman, create the keys below and type in the value in the Body section:

Key     Value
--------------------
name    rob
email   foo@bar.co

Choose POST method and hit the Send button, you get:

{
    "status": 200,
    "data": {
        "changes": [
            {
                "new_val": {
                    "email": "foo@bar.co",
                    "id": "42feb7bc-333b-49a6-89cb-78c788de490c",
                    "name": "rob"
                },
                "old_val": null
            }
        ],
        "deleted": 0,
        "errors": 0,
        "generated_keys": [
            "42feb7bc-333b-49a6-89cb-78c788de490c"
        ],
        "inserted": 1,
        "replaced": 0,
        "skipped": 0,
        "unchanged": 0
    }
}

When you visit http://127.0.0.1:3000/users again, you get:

{"status":200,"data":[{"email":"foo@bar.co","id":"42feb7bc-333b-49a6-89cb-78c788de490c","name":"rob"}]}

You can add more users in and when you just want to query a single user, e.g. http://127.0.0.1:3000/users/rob, you get:

{"status":200,"data":{"email":"foo@bar.co","id":"42feb7bc-333b-49a6-89cb-78c788de490c","name":"rob"}}

3. The PUT method

To update an user, we need to create updateUser method:

const updateUser = async(ctx, next) => {
  await next()

  // Get the db connection.
  const connection = await db()

  // Throw the error if the table does not exist.
  var exists = await r.tableList().contains('users').run(connection)
  if (exists === false) {
    ctx.throw(500, 'users table does not exist')
  }

  let body = ctx.request.body || {}

  // Throw the error if no id.
  if (body.id === undefined) {
    ctx.throw(400, 'id is required')
  }

  // Throw the error if no name.
  if (body.name === undefined) {
    ctx.throw(400, 'name is required')
  }

  // Throw the error if no email.
  if (body.email === undefined) {
    ctx.throw(400, 'email is required')
  }

  let objectId = body.id
  let updateQuery = {
    name: body.name,
    email: body.email
  }

  // Update document by id.
  var result = await r.table('users')
    .get(objectId)
    .update(updateQuery, {returnChanges: true})
    .run(connection)

  ctx.body = result
}

Let go back to Postman and update rob by adding the id key to the form:

Key     Value
--------------------
name    rob
email   fooz@bar.co
id      42feb7bc-333b-49a6-89cb-78c788de490c

When you hit the Send button with the PUT method, you get:

{
    "status": 200,
    "data": {
        "changes": [
            {
                "new_val": {
                    "email": "fooz@bar.co",
                    "id": "42feb7bc-333b-49a6-89cb-78c788de490c",
                    "name": "rob"
                },
                "old_val": {
                    "email": "foo@bar.co",
                    "id": "42feb7bc-333b-49a6-89cb-78c788de490c",
                    "name": "rob"
                }
            }
        ],
        "deleted": 0,
        "errors": 0,
        "inserted": 0,
        "replaced": 1,
        "skipped": 0,
        "unchanged": 0
    }
}

4. The DELETE method

Lastly, to delete an user, we will create deleteUser method:

const deleteUser = async(ctx, next) => {
  await next()

  // Get the db connection.
  const connection = await db()

  // Throw the error if the table does not exist.
  var exists = await r.tableList().contains('users').run(connection)
  if (exists === false) {
    ctx.throw(500, 'users table does not exist')
  }

  let body = ctx.request.body || {}

  // Throw the error if no id.
  if (body.id === undefined) {
    ctx.throw(400, 'id is required')
  }

  let objectId = body.id

  // Delete a single document by id.
  var result = await r.table("users")
    .get(objectId)
    .delete()
    .run(connection)

  ctx.body = result
}

We just need provide the id key in Postman to delete rob:

Key     Value
--------------------
id      42feb7bc-333b-49a6-89cb-78c788de490c

When you hit the Send button with the DELETE method, it results:

{
    "status": 200,
    "data": {
        "deleted": 1,
        "errors": 0,
        "inserted": 0,
        "replaced": 0,
        "skipped": 0,
        "unchanged": 0
    }
}

If you look for rob at http://127.0.0.1:3000/users/rob, you should get:

{"status":400,"message":"no user found"}

Conclusion

That's it. We have completed a simple app with Koa and RethinkDB! You can clone or download the source from GitHub. Let me know what you think and if there are any suggestions or improvements, please leave a comment below. Hope this example is helpful if you ever want to develop an app Koa with RethinkDB.

Discover and read more posts from LAU TIAM KOK
get started