Creating a realtime Koa application with RethinkDB, Socket.io and Nuxt

Published Sep 20, 2017
Creating a realtime Koa application with RethinkDB, Socket.io and Nuxt

Introduction

Following up on this article "Creating an application with Koa and RethinkDB", you may want to make use of RethinkDB's realtime functionality. In this article, we are going to see how to glue the existing app with Socket.io and Nuxt.

Socket.io is a JavaScript framework for realtime applications. It enables realtime, bi-directional communication between web clients and servers. If you are new to socket.io, this is a good example to start with.

Nuxt.js is a framework for creating Universal Vue.js Applications. If you are new to Nuxt, you can check out this article "Decoupling the view and controller in your PHP application: Introducing Nuxt". Even though it is written for PHP applications, the basic concept is the same.

Getting started

In my package.json, I now have added more dependencies and scripts:

  "dependencies": {
    "axios": "^0.16.2",
    "cross-env": "^5.0.1",
    "koa": "^2.3.0",
    "koa-bodyparser": "^4.2.0",
    "koa-favicon": "^2.0.0",
    "koa-mount": "^3.0.0",
    "koa-static": "^4.0.1",
    "koa-trie-router": "^2.1.5",
    "nuxt": "^1.0.0-rc3",
    "rethinkdb": "^2.3.3",
    "socket.io": "^2.0.3"
  },
  "devDependencies": {
    "babel-eslint": "^7.2.3",
    "backpack-core": "^0.4.1",
    "mocha": "^3.5.0",
    "supertest": "^3.0.0"
  },
  "scripts": {
    "test": "cross-env NODE_PATH=./server:./server/modules NODE_ENV=test mocha --compilers js:babel-core/register --recursive",
    "dev": "backpack dev",
    "build": "nuxt build && backpack build",
    "start": "cross-env NODE_ENV=production node build/main.js"
  },

In my app root, I have these additional files:

  1. nuxt.config.js:
module.exports = {
  /*
  ** Headers of the page
  */
  head: {
    title: 'starter',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'Nuxt.js project' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  env: {
    HOST_URL: process.env.HOST_URL || 'http://127.0.0.1:3000'
  },
  /*
  ** Global CSS
  */
  css: ['~/assets/css/main.css']
}
  1. backpack.config.js:
module.exports = {
  webpack: (config, options, webpack) => {
    config.entry.main = './server/index.js',

    // Add NODE_PATH to webpack.
    config.resolve.modules = ['./server']
    return config
  }
}
  1. .babelrc:
{
  "presets": ["es2015"]
}

After having installed the dependencies:

$ npm install

We can start the app in the development environment:

$ npm run dev

Gluing the packages together

Now, follow these steps below to glue Koa, RethinkDB, Socket.io and Nuxt together for our development:

1. Importing new packages

In our existing Koa app:

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

Add in these additional packages:

import mount from 'koa-mount'
import socket from 'socket.io'
import http from 'http'
import { Nuxt, Builder } from 'nuxt'

2. Instantiating Nuxt

After making the instances of Koa and Router:

const app = new Koa()
const router = new Router()
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || config.server.port

Adding Nuxt configuration and passing it to the instance of Nuxt:

// Import and Set Nuxt.js options
let nuxtConfig = require('../nuxt.config.js')
nuxtConfig.dev = !(app.env === 'production')

// Instantiate nuxt.js
const nuxt = new Nuxt(nuxtConfig)

// Build in development
if (nuxtConfig.dev) {
  const builder = new Builder(nuxt)
  builder.build().catch(e => {
    console.error(e) // eslint-disable-line no-console
    process.exit(1)
  })
}

3. Instantiating Socket.io

const server = http.createServer(app.callback())
const io = socket(server)

4. Mounting the existing route to '/api'

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

app.use(mount('/api', router.middleware()))

5. Integrating Koa with Nuxt

After the routes, add this block of code:

app.use(ctx => {
  ctx.status = 200 // koa defaults to 404 when it sees that status is unset

  return new Promise((resolve, reject) => {
    ctx.res.on('close', resolve)
    ctx.res.on('finish', resolve)
    nuxt.render(ctx.req, ctx.res, promise => {
      // nuxt.render passes a rejected promise into callback on error.
      promise.then(resolve).catch(reject)
    })
  })
})

Coding the socket on server

To integrate socket and rethinkdb, you just need to create a new method call listenChanges:

const listenChanges = async(connection) => {
  var cursor = await r.table('users')
    .changes()
    .run(connection)

  io.sockets.on('connection', (socket) => {
    console.log('a user connected')
    socket.on('disconnect', () => {
      console.log('user disconnected')
    })
    cursor.each(function (err, row) {
      if (err) throw err
      console.log(JSON.stringify(row, null, 2))
      socket.emit('users.changed', row)
    })
  })
}

Add it to the existing getUsers method:

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

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

That's it for the server side. When there is a change in the users table, the socket on the server emits the data to 'users.changed'.

Coding the socket on client

Then we need a client to listen to the data emitted from the server. Let's create a client template called index.vue in pages/users/:

<template>
  <div>
    <h1 class="title">
        USERS
      </h1>
      <ul class="users">
        <li v-for="(user, index) in users" :key="index" class="user">
          <nuxt-link :to="'users/' + user.name">
            {{ user.name }}
          </nuxt-link>
        </li>
      </ul>
  </div>
</template>

<script>
import axios from '~/plugins/axios'
import socket from '~/plugins/socket.io'
export default {
  async asyncData () {
    let { data } = await axios.get('/api/users')
    return {
      users: data.data
    }
  },
  head () {
    return {
      title: 'Users'
    }
  },
  created () {
    socket.on('users.changed', function(data) {
      // Make sure there are new_val & old_val in data.
      if (data.new_val === undefined && data.old_val === undefined) {
        return
      }
      // Push the new user in.
      if(data.old_val === null && data.new_val !== null) {
        this.users.push(data.new_val)
      }
      // Pop off the deleted user.
      if(data.new_val === null && data.old_val !== null) {
        var id = data.old_val.id
        // Find index of the deleted item.
        var index = this.users.map(function(el) {
          return el.id
        }).indexOf(id)
        this.users.splice(index, 1)
      }
      // Update the current user.
      if(data.new_val !== null && data.old_val !== null) {
        var id = data.new_val.id
        // Another method finding index of an item.
        var index = this.users.findIndex(item => item.id === id)
        this.users.splice(index, 1, data.new_val)
      }
    }.bind(this))
  }
}
</script>

The socket on the client listens at 'users.changed', and passes the received data to the if-condition blocks. Then we either push or splice the data to this.users.

Using the application

1. Posting the data

If you still have the app running, you should see the screen below at http://127.0.0.1:3000/users.

user-01.png

You only see the title 'USERS', this is because there no user in the users table yet. You also can check the JSON output at http://127.0.0.1:3000/api/user, you get:

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

So, let's add some users in. If you go to Google Postman, make sure that the URL to have this address http://127.0.0.1:3000/api/users, then 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": {
        "deleted": 0,
        "errors": 0,
        "generated_keys": [
            "19684ef8-2c96-4a47-bac6-d71521f73117"
        ],
        "inserted": 1,
        "replaced": 0,
        "skipped": 0,
        "unchanged": 0
    }
}

You should see the screen below at http://127.0.0.1:3000/users - without having to refresh your browser:

user-02.png

If you have multiple screens open already on your browser at http://127.0.0.1:3000/users, you should all screens are being updated with the data emitted from the server.

2. Updating the data

It is the same when you update the user on Postman with the PUT method, providing that you have the id key in the form:

Key     Value
--------------------
name    robbie
email   foo@bar.co
id      19684ef8-2c96-4a47-bac6-d71521f73117

You should see the name rob on all the screens have been updated to robbie automatically.

3. Deleting the data

When you want to delete the user, just provide the id key in Postman with DELETE method:

Key     Value
--------------------
id      19684ef8-2c96-4a47-bac6-d71521f73117

Then you will see this user being removed from the screen concurrantly without having to refresh the browser.

Conclusion

It is quite a task to set up the development environment and gluing the packages together. But once we have them sorted, the rest is like a breeze. You can clone or download the source from GitHub. Let me know what you think and if there are any suggestions or errors, please leave a comment below. Hope this basic example is helpful if you are ever interested in developing a realtime app.

Discover and read more posts from LAU TIAM KOK
get started
Enjoy this post?

Leave a like and comment for LAU