Introduction

Hello and welcome to Day 2 of our Koa.js tutorial where we build a web service that searches guitar amps by music genre. Yesterday we spent some time learning what Koa is then we laid out the groundwork for building our app.

So now, it's time to begin writing out some actual code for our application. Let's get started.

Getting Started

Let's open our terminal and create our app.js file.

cd /your/app/root/directory
touch app.js

Good, this app.js file will be what we use as the core server configuration for our whole application. We'll be importing our needed modules, configure middleware, set up our database, write our routes, etc. from here.

Let's open up app.js and get started.

// import core modules
const Promise = require('bluebird')
const koa = require('koa')
const mongoose = Promise.promisifyAll(require('mongoose'))
const config = require('./config')

First we're just importing some core modules that will be needed. Some astute developers may notice we're importing Bluebird and using it for Promises... but why would we need anything to do with Promises? This is because we are promisifying a couple of libraries, including Mongoose, and under the hood, async/await returns Promises, so in our case I just prefer having Bluebird be the engine for running those Promises. That said, it isn't required, for async/await, just an optional add on I like to incorporate. Moving forward, you'll see Koa and Mongoose being imported. Then lastly, you'll see that config.js file we wrote yesterday.

On to the next step.

// import middleware config
const convert = require('koa-convert')
const router = require('koa-simple-router')
const views = require('koa-views')
const stylus = require('koa-stylus')
const serveStatic = require('koa-static')

// initialize koa server
const app = new koa()

So you will see now we're just importing our middleware modules. We have koa-convert which we'll use to handle one library that was meant for an older version of Koa. Then we have our simple router. That module should be fairly self explanatory, we'll configure our routing options with it. Next, we have koa-viewswhich is the module used for rendering our view files. It has a whole host of HTML pre-processors baked in to give you as many options as you wish. In our case, we'll use Pug but you can use whatever you wish. Next is our stylus module which will allow us to render any Stylus files into CSS. Then we have koa-static which will be instrumental in serving our static assets. Its purpose will become clear in just a minute. Lastly, we initialize our Koa server so that we can begin implementing our middleware.

Before we begin writing that middleware though, we need to configure our database connection. Let's get this handled right quick.

/*
  Mongoose Config
*/

mongoose.Promise = require('bluebird')
mongoose
.connect(config.mongoUrl)
.then(response => {
  console.log('connected to mongo :-)')
})
.catch((err) => {
  console.log("Error connecting to Mongo")
  console.log(err)
})
// define amp schema
const ampSchema = mongoose.Schema({
  name: String,
  genres: Array,
  image: String
})
// define amp model
const Amp = Promise.promisifyAll(mongoose.model('Amp', ampSchema))

Here, we configured Mongoose to use Bluebird's Promise engine. Then we use Mongoose to make our connection to our Mongo database that we defined in config.js. Next, we defined our ampSchema then the Amp model.

Now that we have handled this, it's time to write the first half of our middleware. Let's dive in.

/*
  Server Config
*/
// error handling
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    if (err.status === 401) {
      ctx.status = 500
      ctx.render('error', {
        title: 'AmpBuddy Error',
        errorMessage: "There has been an error processing your request."
      })
    }
    else if (err.status === 501) {
      ctx.render('error', {
        title: 'AmpBuddy Error',
        errorMessage: err.message
      })
    } else {
      ctx.app.emit('error', err, ctx)
      ctx.render('error', {
        title: 'AmpBuddy Error',
        errorMessage: err.message
      })
    }
  }
})

// logger
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}`)
})

Now we have our first two middlewares. This first one is our own handwritten error handler. There is a lot going on with this error handler so we'll take this little by little. First notice how we're passing an async function into our app.use(). Next you'll see we have the parameters ctx and next. ctx is a Koa context. It contains any request and response information along with an assortment of methods you can use as needed. next is a call that will suspend the operation of the current function then pass control to the next function downstream. So knowing this, we can better understand what we are getting into. Now you will see we have the whole operation wrapped in a try/catch and then you will see await next(). We will return to the await next() in just a second, let's take a look at the rest of this function. In a nutshell, we're checking the error status, then sending the proper error status back to the user along with rendering the error page that we haven't yet built yet. Nothing too fancy. So now, what is the deal with this await next() and how does this error handler function in the overall scheme of the app? This is where it is critical to understand the downstream/upstream nature of Koa.

In Koa, when a request is made, the request is encapsulated into a Context object that is then passed downstream through a series of middleware. However, once the object has reached the end of the middleware lineup, it then runs back through the middleware chain to complete any operations that may have been halted. So what does this have to do with our error handler and that await next()? Well, you know how next() passes control to the next function downstream right? What we are doing is suspending the operation of the error handler function until everything else has finished running. If any other middleware, such as our router or logger, throws an error... it will find its way back to our error handler where it will then do its job. If there is no error... nothing happens.

Now, let's turn our attention to the much simpler logger middleware. Here we are creating a new Date, then we pause the function then once the middleware chain finishes and the Context object returns back upstream, we calculate how long the request took before logging the request method, url, and the time the request took to complete.

Conclusion

This was the first half of our application server code. Admittedly, it was still a lot to take in. The core concepts of Koa, specifically the Context object and the downstream/upstream nature of the middleware flow can be tricky to comprehend at first. My hope is that at least now, the concepts have been made a little more approachable.

Tomorrow, we will finish our server configuration before we take on writing our views.

Helpful Links

  1. http://koajs.com/#application
  2. https://github.com/projekt-matara/ampbuddy