Koa

Node.js web server framework

SydJS, March 2014

Marcin Szczepanski (@MarcinS)

Me

Why am I talking about Koa?

What is Koa?

  • Next generation web framework for Node.js
  • From same team as Express
  • Uses ES6 Generators instead of callbacks

ES6 generators aren't in Node stable?

Correct!

Requires Node.js v0.11

node --harmony

How does Koa compare to Express?

Express

  • Built on Connect for callback based middleware framework
  • HTTP helpers (content negotiation, cookies, etc)
  • Routing and views, and other bundled middlware (pre-4.0)

How does Koa compare to Express?

Koa

  • ES6 Generator based middleware framework
  • HTTP helpers (content negotiation, cookies, etc)
  • No bundled middleware

Yet another introduction to ES6 Generators!

Generators in a nutshell

Suspendable functions / Continuations

function*

yield

Basic example


var generatorFunc = function* () {
  yield "Hello";
  return "World";
}

var generatorObj = generatorFunc();
generatorObj.next(); // {value: "Hello", done: false}
generatorObj.next(); // {value: "World", done: true}
generatorObj.next(); // Error: Generator has already finished

Infinite sequences


var generatorFunc = function* () {
  var i = 0;
  while (true) {
      yield i;
      i++;
  }
}

var generatorObj = generatorFunc();
            
generatorObj.next(); // {value: 0, done: false}
generatorObj.next(); // {value: 1, done: true}
generatorObj.next(); // {value: 2, done: true}
generatorObj.next(); // {value: 3, done: true}
generatorObj.next(); // {value: 4, done: true}
generatorObj.next(); // {value: 5, done: true}
generatorObj.next(); // ...

Passing values back into a generator


var generatorFunc = function* () {
  var nextUp = yield "Hello!";
  yield nextUp.toLowerCase();
  return "World!";
}
var generatorObj = generatorFunc();
generatorObj.next(); // {value: 'Hello!', done: false}
generatorObj.next('OK'); // {value: 'ok', done: false}
generatorObj.next(); // {value: 'World!', done: true}

Practical use..


function doStuff(callback) {
  var a,b;
  // $.get returns a promise
  $.get('http://servicea.com/').then(function (result) {
    a = result;
    return $.get('http://serviceb.com/?q=' + a);
  }).then(function (result) {
    b = result;
    // callback is node style function(err, ret)
    callback(null, [a,b]);
  }).catch(function (err) {
    callback(err);
  });
}
            

var co = require('co');
co(function* () {
  var a = yield $.get('http://servicea.com');
  var b = yield $.get('http://serviceb.com?q=' + a);
  return [a,b];
})(callback);
            

co

Supported yieldables:

  • promises
  • thunks (functions)

    eg. fs.readFile:

    function read(path, encoding) {
      return function(cb){
        fs.readFile(path, encoding, cb);
      }
    }
    co(function* () {
      var fileContent = yield read(path, encoding);
    })();
  • array or object
  • generators or generator functions

Back to Koa!

Koa

Middleware based

  • Do something
  • Yield to the next middleware
  • Do something else later

Defining Middleware


app.use(function* (next) {
  // ...
  yield next;
  // ...
});
            

Minimal Koa app


var koa = require('koa'),
    app = koa();

app.use(function* (next) {
  this.response.body = "Hello World!";
  yield next;
});

app.listen(3000);
            

Middleware example: Timing a request


app.use(function* _timingMiddleware(next) {
  var requestStartTime = +new Date();
  yield next;
  var requestDuration = (+new Date()) - requestStartTime;
  console.log(this.request.method, this.request.path, 
    "(" + requestDuration + "ms)");
});

var delay = function (timeout) {
  return function (cb) {
    setTimeout(cb, timeout);
  };
};

app.use(function* _delayMiddleware(next) {
  yield delay(1000);
  yield next;
});

app.use(function* _mainMiddlware(next) {
  this.response.body ="

Hello world!

"; yield next; });

Koa Context

  • Bound to this in middlware
  • Contains request and response objects
  • Contains app reference and cookies helper
  • Contains throw function: this.throw(500, 'Something failed!');

context

Request aliases

  • ctx.header
  • ctx.method
  • ctx.method=
  • ctx.url
  • ctx.url=
  • ctx.path
  • ctx.path=
  • ctx.query
  • ctx.query=
  • ctx.querystring
  • ctx.querystring=
  • ctx.type
  • ctx.host
  • ctx.host=
  • ctx.fresh
  • ctx.stale
  • ctx.socket
  • ctx.protocol
  • ctx.secure
  • ctx.ip
  • ctx.ips
  • ctx.subdomains
  • ctx.is()
  • ctx.accepts()
  • ctx.acceptsEncodings()
  • ctx.acceptsCharsets()
  • ctx.acceptsLanguages()
  • ctx.get()

Response aliases

  • ctx.body
  • ctx.body=
  • ctx.status
  • ctx.status=
  • ctx.length
  • ctx.length=
  • ctx.type
  • ctx.type=
  • ctx.charset
  • ctx.charset=
  • ctx.headerSent
  • ctx.redirect()
  • ctx.attachment()
  • ctx.set()
  • ctx.remove()
  • ctx.lastModified=
  • ctx.etag=

this.request

  • host / method / url / path / query
  • is() - parse Content-Type:
    if (this.is('application/json')) { ... }
  • accepts() - parse Accepts:
    switch (this.accepts('json', 'html', 'text')) {
      case 'json': break;
      case 'html': break;
      case 'text': break;
      default: this.throw(406, 
        'json, html, or text only');
    }
  • Similar functions for acceptsEncodings(), acceptsLanguages(), acceptsCharsets()

this.response

  • body - String, Buffer, Stream or Object
  • type - Content-Type
  • get() set() for headers
  • redirect()
  • status - set to code or string

Status Codes

  • 100 "continue"
  • 101 "switching protocols"
  • 102 "processing"
  • 200 "ok"
  • 201 "created"
  • 202 "accepted"
  • 203 "non-authoritative information"
  • 204 "no content"
  • 205 "reset content"
  • 206 "partial content"
  • 207 "multi-status"
  • 300 "multiple choices"
  • 301 "moved permanently"
  • 302 "moved temporarily"
  • 303 "see other"
  • 304 "not modified"
  • 305 "use proxy"
  • 307 "temporary redirect"
  • 400 "bad request"
  • 401 "unauthorized"
  • 402 "payment required"
  • 403 "forbidden"
  • 404 "not found"
  • 405 "method not allowed"
  • 406 "not acceptable"
  • 407 "proxy authentication required"
  • 408 "request time-out"
  • 409 "conflict"
  • 410 "gone"
  • 411 "length required"
  • 412 "precondition failed"
  • 413 "request entity too large"
  • 414 "request-uri too large"
  • 415 "unsupported media type"
  • 416 "requested range not satisfiable"
  • 417 "expectation failed"
  • 418 "i'm a teapot"
  • 422 "unprocessable entity"
  • 423 "locked"
  • 424 "failed dependency"
  • 425 "unordered collection"
  • 426 "upgrade required"
  • 428 "precondition required"
  • 429 "too many requests"
  • 431 "request header fields too large"
  • 500 "internal server error"
  • 501 "not implemented"
  • 502 "bad gateway"
  • 503 "service unavailable"
  • 504 "gateway time-out"
  • 505 "http version not supported"
  • 506 "variant also negotiates"
  • 507 "insufficient storage"
  • 509 "bandwidth limit exceeded"
  • 510 "not extended"
  • 511 "network authentication required"

RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)

(April 1, 1998)

RFC 2324: HTCPCP/1.0

2.3.2 418 I'm a teapot

Any attempt to brew coffee with a teapot should result in the error code "418 I'm a teapot". The resulting entity body MAY be short and stout.

... but we digress

Complete app using request/response


var koa = require('koa'),
    app = koa();

app.use(function* (next) {
  if (this.path === '/') {
    this.body = "

Home Page

Some info

"; } else if (this.path === '/info') { switch(this.accepts('html', 'json')) { case 'html': this.body = "

Marcin

"; break; case 'json': this.body = { name: 'Marcin'}; break; } } yield next; }); app.listen(3000);

$ curl -i http://localhost:3000/info
HTTP/1.1 200 OK
X-Powered-By: koa
Content-Type: text/html; charset=utf-8
Content-Length: 15
Date: Sun, 09 Mar 2014 20:32:34 GMT
Connection: keep-alive

Marcin


$ curl -i -H"Accept: application/json" http://localhost:3000/info
HTTP/1.1 200 OK
X-Powered-By: koa
Content-Type: application/json
Content-Length: 23
Date: Sun, 09 Mar 2014 20:27:45 GMT
Connection: keep-alive

{
  "name": "Marcin "
}
            

Middleware

What you'll want to build a web application

  • File serving (static files)
  • Routing (URLs)
  • Templates / views
  • Sessions
  • Logging

File serving

koa-static


var serve = require('koa-static');
app.use(serve('./public'));
            

Routing: Simple

koa-route


var route = require('koa-route'),
    myController = require('./myController');

app.use(route.get('/foo', myController.foo));
app.use(route.post('/foo/new', myController.newFoo));
            

Routing: Complex

koa-router


var router = require('koa-router'),
    myController = require('./myController');

app.use(router(app));

app.get('/foo', myController.foo);
app.post('/foo/new', myController.newFoo);
            

var Router = require('koa-router'),
    mount = require('koa-mount'),
    myController = require('./myController');

var router = new Router();
router.get('/', myController.foo);
router.post('/new', myController.newFoo);

app.use(mount('/foo', router.middleware());
            

Templating / Rendering

koa-views


var views = require('koa-views');
// views(basePath, engine)
app.use(views('./views', 'jade'));

app.use(function* (next) {
  // renders views/index.jade
  yield this.render('index', {name: 'Marcin'});
});
            

Sessions

There's a fair few of these...

koa-session


var session = require('koa-session');
app.use(session());
app.use(function* () {
  var views = this.session.views || 0;
  this.session.views = ++views;
  this.body = "Number of views: " + views;
});
            

Logging

Again, there's a few of these...

koa-logger


var logger = require('koa-logger');
app.use(logger());
            

mongodb-logger, koa-log4js, ...

Middleware based example


var koa = require('koa'),
    logger = require('koa-logger'),
    serve = require('koa-static'),
    router = require('koa-router'),
    views = require('koa-views'),
    app = koa();

app.use(logger());
app.use(serve('./public'));
app.use(views('./views', 'jade'));
app.use(router(app));

['infoController', 'homeController'].forEach(function (controller) {
  var controllerModule = require('./controllers/' + controller);
  controllerModule.register(app);
});

infoController


var info = function* (next) {
  var person = {
    name: 'Marcin'
  };
  switch(this.accepts('html', 'json')) {
    case 'html':
      yield this.render('info', person);
      break;
    case 'json':
      this.body = person;
      break;
  }
  yield next;
};

module.exports.register = function (app) {
  app.get('/info', info);
};

Other middleware of interest

  • Authentication - koa-passport?
  • koa-body: body parsing (form post, etc)
  • koa-cors, koa-csrf
  • koa-common: basic standard set of middlwware
  • ... and more on the wiki

... and that's a brief introduction to Koa

Resources

Me: @MarcinS

The End