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-views
which 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
- http://koajs.com/#application
- https://github.com/projekt-matara/ampbuddy
Comments for Koa.js Core Server Configuration