Build an API with Koa.js

Intro

Node.js…

The term evokes different reactions depending on who you are talking with. For some, it represents a means for quickly building lightweight and blazing fast apps. For others, it represents messy callback code. That said, there have been numerous attempts at resolving the dreaded callback code issue. We’ve seen async libraries, Promises, generator functions, and now we have async/await functions. However, most libraries and frameworks still depend on the callback model to get things done. Wouldn’t it be nice to have a Node framework that was built to use these newer Javascript features? This is where Koa fits in.

Koa is a minimalist web framework in the same vein as Sinatra for Ruby or Express for Node. For those experienced with Express, a look at how Koa operates may even feel familiar. In the beginning, Koa was built to run on generator functions instead of callbacks which allowed for a cleaner coding experience. However, as Javascript and Node evolved, so has Koa. After the introduction of async/await functions, version two of Koa was released. The benefits of this approach have been innumerable. Asynchronous code written in Node is now easier to test, debug, read, and understand when used properly.

To demonstrate, we will walk through a simple To-Do API using Koa version 2 with MongoDB.

Setup

Before we go too far, there are a few system requirements which will be necessary for those who wish to follow along in running this demo API. Go through the following list and install or update anything that you are missing.

  1. Node.js 7.x.x or above.

  2. Yarn though pure npm can work as well.

  3. MongoDB

  4. Nodemon

  5. Optional but greatly helpful - Postman

Next, you will need to go to the Koa To-Do Demo repository on Github. Follow the instructions in the readme to get the example downloaded, configured, and running on your system. Once you have completed everything, you should be able to follow along with the walkthrough.

App Structure

ls
koa-demo
  controller
  model
  node_modules
  .gitignore
  app.js
  koa-demo.postman_collection.json
  LICENSE
  package.json
  README.md
  yarn.lock

Upon opening the project in your favorite text editor, you should see the structure shown above. App.js is where the server and Mongo configuration lives. Otherwise, we will be in the controller and model directories.

app.js

App.js is broken down into three sections. First, we include the modules, then the configuration for Mongoose, and lastly the server configuration. Let’s dive in and see what we have going on.

const koa = require('koa')
const mongoose = require('mongoose')
const convert = require('koa-convert')
const bodyParser = require('koa-bodyparser')
const router = require('koa-simple-router')
const error = require('koa-json-error')
const logger = require('koa-logger')
const koaRes = require('koa-res')
const handleError = require('koa-handle-error')
const task = require('./controller/task')
const app = new koa()

As you can see, nothing exotic or special yet. We’re importing Koa, Mongoose, the Task controller, and numerous other middleware to handle various tasks. Lastly, we initialize an empty Koa application object which we will later configure with the middleware and routing that we need.

/*
    Mongoose Config
*/
mongoose.Promise = require('bluebird')
mongoose
.connect('/path/to/your/mongo')
.then((response) => {
    console.log('mongo connection created')
})
.catch((err) => {
    console.log("Error connecting to Mongo")
    console.log(err);
})

Again, nothing out of the ordinary. We’re just making Bluebird into Mongoose’s default Promise engine which will be relevant later. Then we are connecting to MongoDB.


/*
    Server Config
*/
// error handling
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = err.message
    ctx.app.emit('error', err, ctx)
  }
})
// logging
app.use(logger())
// body parsing
app.use(bodyParser())
// format response as JSON
app.use(convert(koaRes()))
// configure router
app.use(router(_ => {
    _.get('/saysomething', async (ctx) => {
        ctx.body = 'hello world'
    }),
    _.get('/throwerror', async (ctx) => {
        throw new Error('Aghh! An error!')
    }),
    _.get('/tasks', task.getTasks),
    _.post('/task', task.createTask),
    _.put('/task', task.updateTask),
    _.delete('/task', task.deleteTask),
    _.post('/task/multi', task.createConcurrentTasks),
    _.delete('/task/multi', task.deleteConcurrentTasks)
}))

app.listen(3000)

Now we are in the server configuration. At first, this looks just like something you would see in Express. We’re just plugging middleware into our stack. However, notice that we are passing async functions to our middleware. It is crucial to note their parameters. In Koa, the ctx stands for a Koa context. A context represents an object that contains both the Node request and response objects along with other helpful methods. The next parameter suspends the function then passes control to the next middleware in line. When there is no middleware left to execute, the stack will then unwind and finish the job of any middleware that has not finished yet.

The server configuration is fairly straightforward. The error handler deals with any errors that are thrown by any other middleware in the stack. Otherwise, let’s turn our attention to the router configuration. It supports the series of HTTP verbs as seen with _.get, _.post, etc. The first parameter is your URL definition, then you pass your async function with the Koa context. The /saySomething route is your obligatory “hello world”. It sets the value of ctx.body to “hello world” which will be sent back as a response to the client. You will also see a “throwError” route which is just there if you wish to test the error handler discussed earlier. From there, we have our routes created for the ToDo routes themselves. Now, let’s turn our attention to the rest of the API.

model/Task.js

const mongoose = require('mongoose')
/*
    Task Schema
*/
const TaskSchema = mongoose.Schema = {
    name: String,
    urgency: String
}

module.exports = mongoose.model("Task", TaskSchema)

Our ToDo API will have a series of Tasks which will be saved to the database. For demonstration purposes, we have a simple model put together in standard Mongoose fashion.

controller/task.js

Here is where we will write the code that controls the Task model. These are the actual functions that are called from the router configuration back in app.js. For brevity’s sake, we will focus on two functions which illustrate the important points of what Koa can do for you.

const Task = require('../model/Task')
exports.getTasks = async (ctx) => {
    const tasks = await Task.find({})
    if (!tasks) {
        throw new Error("There was an error retrieving your tasks.")
    } else {
        ctx.body = tasks
    }
}

So what is going on here? In the big picture, we are locating every task from the database then passing it to the user. At the first line, we are importing the Task model from earlier. Then at the third line, we set up the getTasks async function to be exported. The fourth line, is a critical moment to understand. Here we are setting the variable tasks to the result of Task.find({}) which returns a Promise. The key thing to notice is the await. The await suspends the function until Task.find() is complete. This gives you the benefit of asynchronous execution while retaining the readability of synchronous code. Afterward, you just check to make sure nothing blew up then send the results to the user.

exports.createConcurrentTasks = async (ctx) => {
    const taskOne = Task.create({
        name: ctx.request.body.nameTaskOne,
        urgency: ctx.request.body.urgencyTaskOne
    })
    const taskTwo = Task.create({
        name: ctx.request.body.nameTaskTwo,
        urgency: ctx.request.body.urgencyTaskTwo
    })
    const [t1, t2] = await Promise.all([taskOne, taskTwo])
    if (!t1 || !t2) {
        throw new Error('Tasks failed to be created.')
    } else {
        ctx.body = {message: 'Tasks created!', taskOne: t1, taskTwo: t2}
    }
}

Now let’s see how to use async/await to run a concurrent operation. This function’s job is to create two tasks simultaneously. Here we have another async function named createConcurrentTasks. Take notice of the variables taskOne and taskTwo. At first, they look like the same scheme from the previous function but now await is missing. Why? Pay attention to the new variables t1 and t2. Here we use Javascript’s de-structuring to create variables t1 and t2. Then we call await Promise.all which will run the operations concurrently. If one operation has an error, it will cancel the whole thing to prevent bad side effects. After this, we make sure nothing blew up and if all is well, then we send a response to the user. It’s important to remember that await is made to receive Promises, which means we can utilize tricks like these to handle concurrent or other operations that were originally reserved for Promises alone.

Summary

I hope this brief walkthrough of Koa’s capabilities has been valuable for you. For more information, please dig through the ToDo example, the Koa documentation, and perhaps someday Koa can become a useful addition to your team’s toolbox.