January 19, 2017

Migrate Your Express App to Koa 2.0

by Samier Saeed

koa

I Don't Even Know What Koa 1.0 Is

Koa is a more evolved form of Express whose main feature is the elimination of callbacks.

With Koa 1.0, this was achieved through the use of generator functions. While that was a massive improvement, Koa 2.0 leverages an ES7 feature that is even more intuitive: async/await. This eliminates "callback hell", a problem that hasn't stopped Node from shooting to the top of hot languages liked by both corporations and hackers, but which can end up producing code so unreadable it would be immediately trashed in other contexts.

Let's take a look at a concrete example of what Koa delivers - after all, if pictures are worth a thousand words, how many is a code block worth?

app.get('/stuff', function (req, res) {  
    stuff.get(function (stuff, err) {
        if (err) {
            res.status(500).send("woah sorry, there was an error");
        }

        calculator.doSomething(stuff, function(result, err){
             if (err) {
                res.status(500).send("woah sorry, there was an error");
             }

            res.send(stuff);
        });

    });
});
router.get('/stuff', async (ctx) => {  
    try {
        let theStuff = await stuff.get();
        let res = await calculator.doSomething(stuff);
        ctx.body = res;
    }

    catch (err){
        ctx.status = 500;
        ctx.message = "woah sorry, there was an error";
    }
});

Amazing, isn't it? Aesthetically pleasing, logical, readable, intelligible. Like a utopian language proposal or spec that would never see the light of implementation --- except it has.

Naturally, if you're a NodeJS person in the industry, who writes NodeJS and Express apps for a living, you might be thinking "great, maybe the next time I'm ready to start a project fresh and don't have a time constraint, I can try Koa --- too bad I'll have probably forgotten about it and this article by then". But my friends, you don't have to wait. Since Koa is simply Express reimagined (and was even at one point just the next version of Express before the change was deemed too jarring and breaking to remain the same project), migrating an extant Express app to Koa is possible.

Protect Your Node.js App

Let's use a small example project here to tackle the main difficulties in switching over to Koa: converting your mass of Express middleware to Koa, and interfacing with callback based code. This isn't a step-by-step guide, but a reference point to help someone who wants to see what moving to Koa might entail. Follow along with the example code.

app.js

The app/init/entry-point for Express apps and Koa apps are very similar.

import express from "express";  
import bodyParser from "body-parser";

import logic from "./logic";  
import expressMiddleware from "../common/middleware";

const app = express();

app.use(bodyParser.json());  
app.use(expressMiddleware);

app.get('/', function (req, res) {  
    res.send('Hello World');
});

app.post('/messages', function (req, res) {  
    logic.create(req.body, function (err, item) {
        if (err) {
            return next(err);
        }

        res.send(`item id: ${item._id}`);
    });
});

app.get('/messages/:id', function (req, res, next) {  
    logic.get(req.params.id, function (err, item) {
        if (err) {
            return next(err);
        }

        res.setHeader('Content-Type', 'application/json');
        res.send(JSON.stringify(item));
    });
});

//marks a message as read
app.put('/messages/:id', function (req, res) {  
    logic.markAsRead(req.params.id, function (err) {
        if (err) {
            return next(err);
        }
    });
});

app.get('/messages', function (req, res) {  
    if (!req.isAdmin) {
        return next(new Error("Unauthorized"));
    }

    logic.getAll(function (err, messages) {
        if (err) {
            return next(err);
        }

        res.setHeader('Content-Type', 'application/json');
        res.send(JSON.stringify(messages));
    });
});

app.listen(3000);  
console.log("running on port 3000");  
import Koa from "koa";  
import Router from "koa-router";  
import bodyParser from "koa-better-body";  
import c2k from "koa-connect";

import logic from "./logic";  
import expressMiddleware from "../common/middleware";

const app = new Koa();

app.use(bodyParser());

const convertedMiddleware = c2k(expressMiddleware);

app.use(convertedMiddleware);

app.use(async (ctx, next) => {  
    try {
        await next();
    }

    catch (err) {
        ctx.status = 500;
        ctx.message = err.message || "Sorry, an error has occurred.";
    }
});

const router = new Router();

router.post('/messages', async (ctx) => {  
    let item = await logic.create(ctx.request.fields);
    ctx.body = `item id: ${item._id}`;
});

router.get('/messages/:id', async (ctx) => {  
    let item = await logic.get(ctx.params.id);
    ctx.body = item;
});

router.get('/messages', async (ctx) => {  
    if (!ctx.req.isAdmin) {
        throw Error("Unauthorized");
    }

    let messages = await logic.getAll();
    ctx.body = messages;
});

app.use(router.routes()).use(router.allowedMethods());

//default, index
app.use((ctx) => {  
    ctx.body = "Hello World";
});

app.listen(3000);  
console.log("running on port 3000");  

There are a couple minor differences when it comes to things like routing (and the fact that I opted to use ES6 instead of commonjs syntax for modules since we will have to use Babel anyways), but the most important things to point out are the way we use Express middleware in a Koa application and how much cleaner the controller logic is when we don't have to constantly pass anonymous functions down to lower layers of the application.

Note that most of the time Koa endpoints are async functions, since that's what gives us the ability to avoid callbacks. However, when that's not necessary, we can use a normal function, as we do for our index endpoint.

Using Express Middleware

As you can see, we use the koa-connect library to convert middleware defined with Express's middleware signature to Koa compatible middleware. You should do that for every custom middleware function you need to migrate.

You might need to do it for several middleware functions from external sources as well; however, as you can already see from the request body parser we use in our Koa app, a great many Express middleware libraries have Koa counterparts already made for you. For a good, but not comprehensive, list, see here.

Note that the writer of koa-connect has not published v2 (which is compatible with Koa v2) to NPM, and you will have to install it like so npm install --save git+https://github.com/vkurchatkin/koa-connect.git.

A note on Koa middleware:

The signature for Koa middleware looks like this:

async function(ctx, next){  
    //do something with ctx on way in
    await next();
    //do something with ctx on way out
}

As with Express, third-party Koa middleware libraries provide the consumer with "factories", functions that take in options as parameters and return a middleware function. Your own custom middleware you write for Koa should conform to this pattern for consistency, even if they don't need options.

Interfacing With Callback Based Code

While switching Express middleware to Koa format is fairly straightforward through a combination of koa-connect and simple refactoring, dealing with layers of callback based code can be tricky.

In our simple application, we start off using the native JavaScript Promise in the following function from the Koa app:

create: function (payload) {  
        payload.read = false;
        return new Promise((resolve, reject) => {
            db.insert(payload, (err, item) => {
                if (err) {
                    reject(err);
                }

                else {
                    resolve(item);
                }
            });
        });
    }

However, that merely pushes the messiness down a layer from where the controller/route is defined to the logic layer. Since this application only uses nedb as an external library, we know that most of the callbacks to pass have the same signature: (error, thing). In fact, this is a very common signature throughout Node development. Hence, we can write the execAsPromise function found in koa/utils.js. This allows us to abstract the Promise part.

Our simple little implementation has a couple problems:

  1. We have to bind db as this every time we use it, and it's a little inelegant.
  2. It only works with a specific callback signature.
  3. As a kind of corollary to 2, it can't handle a dynamic number of callback parameters.

If you need a little more functionality and flexibility, check out es6-promisify.

Using that library, not only can you handle a more diverse range of callbacks, but you can promisify a function once and reuse it.

Now, Promises have been a tried and true method for reducing or eliminating callback-hell in Node applications for some time. What's great about Koa v2's use of async/await is that Promises fit in with the async/await paradigm seamlessly. Compare the following.

function doSomething(p) {  
    exec(p).then((res) => {
        if (res.foo) {
            //do something else
        }
    });
}
async function doSomething(p) {  
    let res = await exec(p);

    if(res.foo) {
        //do something else
    }
}

Both can be used without changing the implementation of exec. So if you have already been using Promises to clean up your code, you will have an even easier time switching to Koa.

A Side Note: Handling Errors

Error handling is definitely far easier and intuitive in Koa than in Express. Here is Express's documentation on error handling. By contrast, there's not that much more to error handling in Koa aside from what we have:

//koa/app.js

app.use(async (ctx, next) => {  
    try {
        await next();
    }

    catch (err) {
        ctx.status = 500;
        ctx.message = err.message || "Sorry, an error has occurred.";
    }
});

try/catch is a trusted and long-standing software paradigm that is straightforward for developers to reason about.

XX

Running a Koa 2.0 Application

Try running koa/app.js--you will get an error. Surely I have led you astray with my penchant for newfangled and bleeding edge JS(!) But no, we simply need a bit more tooling to set things right.

In order to use Koa 2.0, we need to "transpile" our code so that it's interpretable by the current NodeJS runtime. The sample code's dependencies and .babelrc include all you need to achieve this. Use npm run launch to run the Koa app.

next()

Every so often, I'll look at a job description for or get hit up by a recruiter for a NodeJS job. Some of them sound pretty titillating; but then I remember that none of them use Koa. I can't imagine the petty frustration of having to tango with a legacy Express app on a daily basis, and don't ever want to find out. I used to be a NodeJS skeptic who preferred Python, but since discovering Koa, it is now my go-to solution (especially coupled with TypeScript).

If you're interested in learning more about what it would take to switch a full MVC app to Koa, check out koa-mount and koa-serve for info about serving static files.

Protect Your Node.js App

If you're looking for an ORM, I suggest Knex (maybe with Objection), as it is Promise-based and therefore fits perfectly with async/await.

And once your application is up and running, don't forget to secure it! While most people view code protection as a client-side concern, there are several benefits to using Jscrambler on the server side for your Node.js application, including:

  • Code locks which help you enforce license agreements
  • Extra layer of protection against leaks

People complain a lot about "JavaScript fatigue" and the chaotic state of tooling in the JS world, with Babel, Webpack, and transpiling. But there is a very strong benefit to that, which is that we don't need to wait until some slow-moving organization implements something to take advantage of excellent features like async/await. Try Koa today!