The Blog

Building A Server-Side Application With Async Functions and Koa 2 — Smashing Magazine

build The build of the app, containing the compiled code from srcThe src directory contains the following:

api.js The module that defines the app’s API

app.js The module that instantiates and configures the Koa app’s instance

config.js The module that provides the configuration of the app to other modulesIf additional files or modules are needed as the app grows, we would put them in a subfolder of the src folder — for example, src/models for application models, src/routes for more modular API definitions, src/gateways for modules that interact with external services, and so on.

NPM Scripts As A Task Runner

After using Gulp and Grunt as task runners, I came to the conclusion that npm scripts are better than separate tools when working on server-side projects. One of the advantages of npm scripts is that they allow you to invoke locally installed modules as if there were globally installed. I use the following scripts, which need to be defined in package.json:

The start script simply runs index.js. The watch script runs the start script using the nodemon tool, which automatically restarts the server whenever you change something in the app. Note that nodemon is installed as a local development dependency and does not have to be installed globally. The build script compiles the files in the src folder using Babel and outputs the results to the build folder. The test script runs the build script first and then runs tests using mocha. Mocha requires two modules: babel-polyfill, to provide runtime dependencies of the compiled code, and babel-register, to compile the test files before executing them. Additionally, presets for Babel have to be added to package.json, so that you don’t have to provide them in the command line:

{
"babel": {
"presets": [
"es2015",
"stage-3"
]
}
}

This preset enable all ECMAScript 2015 features, as well as features that are currently in stage 3. With this installed and configured, we can start developing the app.

The module reads two environmental variables: PORT and NODE_ENV. NODE_ENV should be either development or production. In development mode, babel-register will be used to compile modules at runtime. babel-register caches the results of the compilation and, thus, reduces the server start time, so that you can iterate faster during development. Because this module is not recommended for production use, the precompiled version from the build directory will be used in production mode. index.js is the only file of the project that will not be compiled by Babel and that must use native module syntax (i.e. CommonJS). Therefore, the app’s instance is located in the default property of the imported app module, which is an ECMAScript 6 module that exports the app’s instance as a default export:

export default app;

This is important to keep in mind if you mix ECMAScript 6 and CommonJS modules. Now to the app.js file itself. This and the other files discussed below are always compiled by Babel in both development and production environments, and the new syntax (including async functions) may be used freely in them:

Here, we are using ECMAScript 2015’s import syntax to import the required modules. Then, we create a new instance of the Koa application and attach several middleware functions to it using the use method. The last thing we do is export the app so that it can be used by index.js. The second middleware function in the chain is an async function and an arrow function at the same time:

In Koa 2, the next parameter is an async function that triggers the next middleware function in the list. Just like in Koa 1, you can control whether the current middleware function should do its job before or after the others by putting the call to next either at the beginning of the current function or at the end. Also, you can catch all errors in the downstream middleware functions by wrapping the await next(); statement in a try/catch statement wherever doing so makes sense.

Defining the API

The api.js file is where the core logic of our app resides. Because Koa does not provide routing out of the box, the app has to use the koa-router module:

import KoaRouter from 'koa-router';
const api = KoaRouter();

Here, koa-router provides functions to define middleware functions for specific HTTP methods and paths — for example, the route that stores events in the database:

Each method can have several handlers, which run sequentially and have exactly the same signature as the middleware functions defined in the top level of app.js. For example, validateKey and validateCollection are simply async functions that validate the incoming request and return 404 or 401 if the provided event collection does not exist or if the API key is not valid:

Note that arrow middleware functions cannot refer to the context of the current request using this (i.e. this is always undefined in the examples thus far). Therefore, request and response objects as well as Koa helpers are available in the context object (ctx). Koa 1 had no separate context object, and this referred to the current request context. After defining other API methods, we finally export the API to be connected to the Koa application in app.js:

export default api;

Persistence Layer

In api.js, we accessed the collections array in the context ctx, which we initialized in app.js. These collection objects are responsible for storing and retrieving data stored in Redis. The Collection class is as follows:

The async/await syntax is really helpful here because it allows us to coordinate several async operations easily — for example, in a loop. But there is one important thing to keep in mind. Let’s look at the _incrGroups method:

Here, the keys are incremented sequentially, meaning that the next key will be incremented once the previous incrementation has succeeded. But this kind of job can be done in parallel! With async/await, the task might not be easy to accomplish, but promises can help:

// Start all increments in parallel
// because there is no await inside the map callback here
const promises = this.groupBy.map(attr =>
db.hincrby(`${this.name}_by_${attr}`, event[attr], 1));
// Wait for all of them to finish
await Promise.all(promises);

The interchangeability of promises and async functions is very helpful.

Testing

The app’s tests are located in the test folder. Let’s look at apiSpec.js, which represents the test specification for the app’s API:

We import expect from chai and supertest. We use the precompiled version of the app and the config to make sure that we are testing exactly the same code that will be running in production. Next, we write the tests for the API, leveraging the async/await syntax to achieve sequential execution of the test steps:

Note that functions passed to it functions are marked with async. This means that await can be used to run asynchronous tasks, including the supertest requests, which return then-able objects and which, therefore, are compatible with what await expects.

Deployment With PM2

Once the app is ready and tests have passed, let’s prepare everything to run the app in production. For this, let’s declare ecosystem.json, which will hold the production configuration of the app:

If your app requires additional servers (for example, a cron job), you can add them to ecosystem.json, and they will be started together with the main server. To start the app on the production server, you can run this:

Conclusion

Babel allows us to build apps using ECMAScript syntax that is not natively available but that includes async/await, which makes writing asynchronous code more enjoyable. The code that uses the async/await syntax is easier to read and maintain. Koa 2 and Babel allow you to start using async functions right now. Nevertheless, Babel brings additional overhead and requires additional configuration and an extra build step. Therefore, waiting until async/await is natively available in Node.js is recommended. Koa 2 should be officially released once this happens. Then, Koa 2 will be a good alternative to Express because it is more modular and simpler and allows for configuration the way you want it. The deployment section of this tutorial might be too simple and unscalable. It leaves open the question of how and when to build and actually deploy the code — you can do this manually (rsync, scp) or set up a continuous integration server for this. Also, the inner architecture of the app is too simple yet is suitable for a demo. Larger and more ambitious apps might require other entities, such as gateways, mappers, repositories and so on, but all of them can leverage async functions. I hope you’ve enjoyed this tutorial. Thanks for reading!