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.
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.
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. Hope you learn something new like I did.