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:
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']
}
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
}
}
.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
.
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:
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. Hope you learn something new like I did.
i have cloned your repo.
successfully running the app, but the data is not automatically pushed in the UI.
i think the socket stuff doesn’t work.
where is the best place to deploy this?
Do you mean a hosting server? If so, ideally own a server yourself and host it.
Otherwise, any commercial server that allows you to install the required software above, such as https://www.linode.com/
yeah sorry i meant hosting. ah, never heard of linode before. thanks :) looking forward to giving this a go. Have you used this in any production apps or bigger projects?
That’s OK. Haven’t used this concept on any apps yet. Hopefully it is my next project. Been using Express but it isn’t good when the app grows.