An Introduction to Building TDD RESTful APIs with Koa 2, Mocha and Chai

Building something with Javascript is relatively easy. Take a look on Medium and you’ll see an uninterrupted stream of articles and tutorials.

But… how many of them covers Test Driven Development with Javascript? Few. Mine included. Testing is hard and more often than not I find myself writing code without even having a test case.

So, what follows is an introduction to building a RESTful API with Koa 2, Mocha and Chai, by following the TDD principles.

Koa 2, Mocha, and Chai

The concept is simple: we’ll build a simple API for displaying and storing a collection of blog articles, code and tests included!

What you will learn

  1. How to start building a RESTful APIs with Koa 2
  2. The basics of Test Driven Development with Mocha, Chai, and Chai HTTP

Disclaimer

The following guide provides some basic examples. It does not cover every possible use case but will give you a good starting point either way.

Also, keep in mind that the code in this tutorial is not suitable for production. Use it at your own risk. Its purpose is to show you the basics. Don’t copy/paste the examples without having a solid understanding about what the code is supposed to do.

Requirements

In order to follow along with this tutorial you should have a basic understanding of Javascript, Node.js, and ES6.

The latest version of Node.js is available from the official website.

If you use Linux, you should absolutely check out Nodesource.

Also, make sure to have at least Node v7.6.0 or higher. In the following examples I’ll make large use of the async/await pattern.

Scaffolding the project

To start the project, create an empty directory named koa-tdd-api:

mkdir koa-tdd-api

then move inside the newly created directory:

cd koa-tdd-api

and initialize package.json by running:

npm init

We won’t publish any module to NPM so you can safely accept the default choices and just move on.

We also need to create some directory structure. In your terminal type:

mkdir -p server/{routes,db,controllers} test

and hit enter.

Setting up Knex and the database

That’s a boring task but someone has to do it: setting up the database and all the likes. Our tools of choice will be:

  1. Sqlite3: it’s enough to get started for a testing/development environment and it offers almost instant feedback
  2. Knex: it’s a SQL query builder for Javascript. To keep things simple I won’t use any ORM in this tutorial but you should take a look at Sequelize right after finishing this project.

Before doing anything else install the Sqlite3 adapter with:

npm i sqlite3@3.1.8 --save-dev

The reason why I picked version 3.1.8 is because of this issue. I hope it will fixed soon.

Right after that add Knex with:

npm i knex

Knex comes included with a command line tool for managing both migrations and seeding. The Knex CLI can also be used to initialize the Knex configuration file. Run in your terminal:

node_modules/knex/bin/cli.js init

and you should see the following output:

Created ./knexfile.js

This file will hold the database credentials for every environment. You can also specify where your migrations and seeds should be generated. Open up knexfile.js and update the configuration by replacing the defaults with the following:

// knexfile.js

const path = require("path");
const BASE_PATH = path.join(__dirname, "server", "db");

module.exports = {
  test: {
    client: "sqlite3",
    connection: {
      filename: "./test.sqlite3"
    },
    migrations: {
      directory: path.join(BASE_PATH, "migrations")
    },
    seeds: {
      directory: path.join(BASE_PATH, "seeds")
    }
  }
};

Of course you’re free to load in other environments such as “dev” or “prod”, anyway for the scope of this tutorial I’ll stick just with a “test” environment.

At this point you can safely generate your first migration with:

node_modules/knex/bin/cli.js migrate:make articles --env test

A new migration will be created inside server/db/migrations/. Open up the newly created file and update the defaults with the following code:

// server/db/migrations/$TIMESTAMP_articles.js

exports.up = function(knex, Promise) {
  return knex.schema.createTable("articles", table => {
    table.increments();
    table.string("title").notNullable().unique();
    table.text("body").notNullable();
    table.string("tags").notNullable();
  });
};

exports.down = function(knex, Promise) {
  return knex.schema.dropTable("articles");
};

The above code defines a new table named articles. Every article will be composed of a title, a body and some tags.

Save and close the file, then commit the migration with:

node_modules/knex/bin/cli.js migrate:latest --env test

A new file, test.sqlite3 will appear inside your project directory. It’s the test database. Eager to see what’s inside? You can browse it with sqlitebrowser even if it’ll be most likely empty right now. Time to load some data!

Seeding the database for testing

Right now the database is empty. We need some kind of data which will be used by the testing suite during its execution. Knex makes easy to create fake data by making use of seeds.

NOTE: Seeding is not meant to be used during testing. That’s a job for fixtures and factories. Seeding is mostly used for populating an application with some data needed to allow the software to be usable as soon it starts. Knex does not have a fixtures feature, therefore we’ll use seeding either way

Generate the seed file by running:

node_modules/knex/bin/cli.js seed:make articles --env test

and a new seed will appear inside server/db/seeds/articles.js. Open up the file and add some fake articles inside the insert() call:

//server/db/seeds/articles.js

exports.seed = function(knex, Promise) {
  return knex("articles")
    .del()
    .then(function() {
      return knex("articles").insert([
        {
          title: "An Introduction to Building Test Driven RESTful APIs with Koa",
          body: "An Introduction to Building Test Driven RESTful APIs with Koa ...",
          tags: "koa, tdd, nodejs"
        },
        {
          title: "Going real time with Socket.IO, Node.Js and React",
          body: "Going real time with Socket.IO, Node.Js and React",
          tags: "socket.io, nodejs, react"
        }
      ]);
    });
};

Now that everything is set let’s start with the serious stuff: setting up the testing suite.

Test Driven Development: make it fail, one test at a time

In brief, Test Driven Development is a programming style based on the repetition of a set of steps:

  1. Starting with a list of requirements, analyze the requested feature and write a simple test case
  2. Make sure the test fails at first: this is the red phase. The test will fail because the feature hasn’t been implemented yet
  3. Write just enough code, in its simplest form, to make sure the test pass: green phase
  4. Refactor the code to remove any duplicate. This is the refactoring phase

Test Driven Development can be easily implemented within Javascript. We will make use of three renowned libraries:

Mocha: it’s a Javascript testing framework with a lot of features. With Mocha you can organize and automate your tests easily

Chai: it’s an assertion library for Javascript. An assertion library is nothing more than a tool for verifying that your code produces the intended result. If you’re new to testing I highly recommend reading the documentation before getting started

Chai HTTP: it’s a plugin for Chai. It makes easy to test APIs and web servers

As usual, install the dependencies with:

npm i mocha chai chai-http --save-dev

and you’re ready to go.

Testing Koa 2 with Mocha, Chai and Chai HTTP

Update the script section inside package.json. The test command should point to mocha:

"scripts": {
  "test": "mocha"
}

Following the TDD principles, let’s start with a simple test case. In this first test we want to make sure that our server will respond to every HTTP requests against the /api/v1/articles endpoint.

Create a new file named articles.routes.test.js inside the test directory:

// test/articles.routes.test.js

// Configure the environment and require Knex
const env = process.env.NODE_ENV || "test";
const config = require("../knexfile")[env];
const server = require("../server/index");
const knex = require("knex")(config);
const PATH = "/api/v1/articles";

// Require and configure the assertion library
const chai = require("chai");
const should = chai.should();
const chaiHttp = require("chai-http");
chai.use(chaiHttp);

// Rollback, commit and populate the test database before each test
describe("routes: articles", () => {
  beforeEach(() => {
    return knex.migrate
      .rollback()
      .then(() => {
        return knex.migrate.latest();
      })
      .then(() => {
        return knex.seed.run();
      });
  });

// Rollback the migration after each test
  afterEach(() => {
    return knex.migrate.rollback();
  });

// Here comes the first test
  describe(`GET ${PATH}`, () => {
    it("should return all the resources", done => {
      chai
        .request(server)
        .get(`${PATH}`)
        .end((err, res) => {
          should.not.exist(err);
          res.status.should.eql(200);
          res.type.should.eql("application/json");
          res.body.data.length.should.eql(2);
          res.body.data[0].should.include.keys("id", "title", "body", "tags");
          done();
        });
    });
  });


/** every subsequent test must be added here !! **/
});

The first two sections of the file serve for the purpose of configuring Knex and requiring the assertion libraries: Chai and its Chai Http plugin.

Right next there are two calls to Knex: the first call rollbacks, commits the migration and populates the test database before each test. The second call to Knex instead makes sure that the test database gets cleaned at the end of every test case.

Right after that comes the first test, starting with the describe block. Both describe and it are Mocha functions.

The describe block helps you organizing a group of similar tests, against a specific endpoint for example.

The it block on the other hand is the actual test being declared.

By looking at the above code it should be clear also how Chai makes possible to spawn the server and subsequently making a get request:

//..
      chai
        .request(server)
        .get(`${PATH}`)
// ..

So what’s the test all about? Speaking of HTTP servers, you may want to check whether a given route responds correctly or not by:

Checking if no errors exists:

// ...
should.not.exist(err);
//...

Checking if the server responds with the expected status code:

// ...
res.status.should.eql(200);
//...

Checking if the server sends back valid JSON:

// ...
res.type.should.eql("application/json");
//...

Checking if the JSON response contains the expected output:

In the example below I’m asserting that the response body contains an array named data, which should have a length equal to 2:

// ...
res.body.data.length.should.eql(2);
//...

and I also want to make sure that the first element of the data array contains the keys “title”, “id”, “body”, and “tags”:

// ...
res.body.data[0].should.include.keys("id", "title", "body", "tags");
//...

It’s worth noting that Chai supports three different assertion styles: the Should style, the Expect style and the Assert style. Picking one over another it’s a matter of personal preferences. Personally I feel the Should style more natural.

With the first test case in place, it’s time to make it fail. To do so, create a new file named index.js inside the server/ directory and put a simple console.log() call inside it:

// server/index.js

console.log("make the test fail");

Ready? Run the test suite with:

npm test

and watch the test fail:

TypeError: app.address is not a function

To make the test pass we should move to the green phase, or rather: write just enough code to make the test pass.

Setting up the Koa server

Why Koa?

Koa is a web framework for Node.js.

How many frameworks for Node.js exist as of today? I’ve lost the count: picking one over another is a tough choice. Lately I’ve been enjoying Koa amongst the others for two simple reasons:

  1. I’ve been using Node 8 since its release and that means I’m able to use the async/await pattern inside Koa 2. My code feels more natural and I’m able to handle errors correctly
  2. Koa is minimalist. It includes only the bare minimum by default. That means less memory consumption and less code to debug

and there is a lot more. Think of it as the Express of the future. I suggest reading the documentation to discover what Koa has to offer. One of its features is the context object for instance.

You will see a lot of references to the ctx object in the upcoming examples. ctx, or context, is an object containing both the Node.js request and response objects. It is specific to Koa and provides a lot of useful methods.

Wiring up the server

In its basic form an API would be composed of three key components, glued together.

A server: it’s the component responsible for listening on a given network port, forwarding all the request to the appropriate route

A set of routes, ie the components responsible for handling the incoming requests. This route for example:

router.get("/api/v1/articles", articlesController.index);

will handle all the requests for GET /api/v1/articles by asking the articlesController.index function to retrieve the appropriate data from the database. You should pay attention to not put too many business logic inside your routes.

For example you may be tempted to write:

// BAD
router.get("/api/v1/articles", async ctx => {
  try {
    const articles = await knex("articles").select();
    ctx.body = {
      data: articles
    };
  } catch (error) {
    console.error(error);
  }
});

The above code will work fine but it’s not immediately understandable as:

// GOD
router.get("/api/v1/articles", articlesController.index);

Finally we have the controller: to put it simply, the controller is responsible for intepreting the request, fetching the requested data and sending an appropriate output back to the user.

Besides some slightly variations between every web framework, those listed above are some of the main building blocks of the MVC pattern. The key difference here is that with Rails, Laravel, Phoenix and co. you get some sort of organization for free. With Javascript and Node.js you won’t have any guidance about the project structure, unless you go with Trails or something similar.

With this basic theory in place let’s start by creating the server. Our goal is still to make the previous test green.

Install Koa with:

npm i koa

open up server/index.js again, remove the existing code and replace it with the following:

// server/index.js

const Koa = require("koa");
const articlesRoutes = require("./routes/articles.routes");

const app = new Koa();
const PORT = process.env.PORT || 8081;

app.use(articlesRoutes.routes());

const server = app.listen(PORT).on("error", err => {
 console.error(err);
});

module.exports = server;

save and close the file, then move on to setting up the routes.

Setting up the Routes and the Controller, ie making the test pass

Koa does not include a router by default. Routing functionalities are provided inside the koa-router package:

npm i koa-router

Setting up GET /api/v1/articles

The first route we’ll going to create is the endpoint for GET /api/v1/articles.

Create a new file named articles.routes.js inside the server/routes directory:

// server/routes/articles.routes.js

const Router = require("koa-router");
const router = new Router();
const articlesController = require("../controllers/articlesController");
const BASE_URL = `/api/v1/articles`;

router.get(`${BASE_URL}`, articlesController.index);

module.exports = router;

Did you noticed how I’m requiring the articlesController inside the article route? Unlike a full MVC framework, Koa does not enforce a strict structure for your project. How the application gets organized is completely up to you so we’d better follow some conventions.

A controller named articlesController should exists and a corresponding action named index should be defined inside the controller as well. The action can be a basic Javascript function: it should fetch the resources from the database and return something meaningful to the user.

Start by creating the controller alongside the action inside server/controllers/articlesController.js:

// server/controllers/articlesController.js

const env = process.env.NODE_ENV || "test";
const config = require("../../knexfile")[env];
const knex = require("knex")(config);

const index = async ctx => {
  try {
    const articles = await knex("articles").select();
    ctx.body = {
      data: articles
    };
  } catch (error) {
    console.error(error);
  }
};

module.exports = { index };

Pay attention to the above async arrow function which takes ctx as a parameter and:

  1. makes a query to the underlying Sqlite3 database by using Knex
  2. appends the data property to the response body

Save and close the file: at this point we have a basic API with a single route, waiting to be tested.

Run the test suite again with:

npm test

and watch your first test pass:

GET /api/v1/articles

Setting up GET /api/v1/articles/:id

Let’s move on by writing another failing test for the next endpoint: GET /api/v1/articles/:id. This route is expected to return a single resource from the database.

Update test/articles.routes.test.js by adding the code below:

// test/articles.routes.test.js
// ...
  describe(`GET ${PATH}/:id`, () => {
    it("should return a single resource", done => {
      chai
        .request(server)
        .get(`${PATH}/1`)
        .end((err, res) => {
          should.not.exist(err);
          res.status.should.eql(200);
          res.type.should.eql("application/json");
          res.body.data.length.should.eql(1);
          res.body.data[0].should.include.keys("id", "title", "body", "tags");
          done();
        });
    });
    it("should return an error when the requested article does not exists", done => {
      chai
        .request(server)
        .get(`${PATH}/9999`)
        .end((err, res) => {
          should.exist(err);
          res.status.should.eql(404);
          res.type.should.eql("application/json");
          res.body.error.should.eql("The requested resource does not exists");
          done();
        });
    });
  });

In the above code we’re basically testing the following behaviours:

  1. the route should return a single resource
  2. the route should return an error when the requested article does not exists

We’re also expecting the correct status codes to be returned from the API.

Run the test suite with:

npm test

and watch it fail. To make the test pass you should update the routes, again.

Open up server/routes/articles.routes.js and add:

// server/routes/articles.routes.js
// ...
router.get(`${BASE_URL}/:id`, articlesController.show);
// ...

update the controller as well by adding the show action inside server/controllers/articlesController.js:

// server/controllers/articlesController.js
// ...
const show = async ctx => {
  try {
    const { id } = ctx.params;
    const article = await knex("articles")
      .select()
      .where({ id });
    if (!article.length) {
      throw new Error("The requested resource does not exists");
    }
    ctx.body = {
      data: article
    };
  } catch (error) {
    ctx.status = 404;
    ctx.body = {
      error: error.message
    };
  }
};

module.exports = { index, show };
// ...

Again, look at the above async arrow function which takes ctx as a parameter and:

  1. makes a query to the underlying Sqlite3 database by using Knex, selecting only the requested id passed via the request parameter
  2. throws an error if the requested resource does not exists
  3. appends the data property to the response body when the requested resource is present

Run the test suite again with:

npm test

and watch the test pass:

GET /api/v1/articles/:id

Setting up POST /api/v1/articles/

Our last test will check the behaviour of the POST /api/v1/articles/ endpoint. The route is expected to create a new resource inside the database by starting from the request body.

Koa doesn’t know how to handle the body by itself, thus we must install koa-bodyparser:

npm i koa-bodyparser

update server/index.js as well in order to use the new middleware:

// server/index.js
// ..
const bodyParser = require("koa-bodyparser");
// ..
app.use(bodyParser());
// Add bodyParser just before the routes call
// ..

Update the tests as well by adding the code below inside test/articles.routes.test.js:

// test/articles.routes.test.js
// ...
  describe(`POST ${PATH}`, () => {
    it("should return the newly added resource identifier alongside a Location header", done => {
      chai
        .request(server)
        .post(`${PATH}`)
        .send({
          title: "A test article",
          body: "The test article's body",
          tags: "test, nodejs"
        })
        .end((err, res) => {
          should.not.exist(err);
          res.status.should.eql(201);
          res.should.have.header("Location");
          res.type.should.eql("application/json");
          res.body.data.length.should.eql(1);
          res.body.data[0].should.be.a("number");
          done();
        });
    });
    it("should return an error when the resource already exists", done => {
      chai
        .request(server)
        .post(`${PATH}`)
        .send({
          title: "An Introduction to Building Test Driven RESTful APIs with Koa",
          body: "An Introduction to Building Test Driven RESTful APIs with Koa ... body",
          tags: "koa, tdd, nodejs"
        })
        .end((err, res) => {
          should.exist(err);
          res.status.should.eql(409);
          res.type.should.eql("application/json");
          res.body.error.should.eql("The resource already exists");
          done();
        });
    });
  });

In the above code we’re testing if:

  1. the route returns the newly created resource identifier, alongside a Location header
  2. the route returns an error when the newly added resource already exists inside the database

We’re also expecting the appropriate status code to be returned from the API:

201 Created: This status code should be returned whenever a new resource has been created. See 201

409 Conflict: This status code should be returned whenever a new resource couldn’t be created because the same one is already present inside the database. See 409

Run the test suite when you’re ready:

npm test

and watch it fail.

Guess what? To make the test pass you must add the corresponding route.

Open up server/routes/articles.routes.js and add:

// ...
router.post(`${BASE_URL}`, articlesController.create);
// ...

update the controller as well by adding the create action inside server/controllers/articlesController.js:

// server/controllers/articlesController.js
// ...
const create = async ctx => {
  try {
    const { body } = ctx.request;
    const article = await knex("articles").insert(body);
    if (!article.length) {
      throw new Error("The resource already exists");
    }
    ctx.status = 201;
    ctx.set("Location", `${ctx.request.URL}/${article[0]}`);
    ctx.body = {
      data: article
    };
  } catch (error) {
    ctx.status = 409;
    ctx.body = {
      error: "The resource already exists"
    };
  }
};

module.exports = { index, show, create };
// ...

The above code has another async arrow function which takes ctx as a parameter and:

  1. adds the new resource inside the database
  2. adds a Location header to the response
  3. appends the data property to the response body
  4. throws an error if the requested resource already exists

Now run the test suite again:

npm test

and watch all the tests pass:

Testing POST /api/v1/articles

You did it!

The pattern should be clear now: write a failing test, add just enough code to make it pass, then refactor. By doing so you will not only be more confident and satisfied with your code, but more importantly, you’ll ship only the necessary logic for making your app work, nothing more, nothing less.

The delete route, next to the corresponding controller’s action would be a nice addition to this simple API: that’s left as an exercise for you.

The code

The code for koa-tdd-api is available on Github: koa-tdd-api

A more complex example with basic authentication is available at faq-app-api

Where to go from here

Have you spotted any error in my code? Any suggestion? I would love to ear your input in the comments below!

Testing is hard and this is why you should practice it: it must become an habit.

It will require time and discipline to incorporate TDD into your workflow, especially if you’re new to Web Development. In fact I encourage you to begin with TDD as soon as possible: the benefits will be invaluable.

If you’re absolutely new to TDD I suggest starting with Why learning TDD is hard, and what to do about it.

After you get a grasp of how TDD works, start immediately by creating tests: as a rule of thumb, next time you’re about to write some code stop it and write a test case first. Make it fail and only then you can start adding the appropriate code to make the test pass.

Once you’re confortable with the basics, go learn about Unit and Integration Testing too!

Thanks for reading! See you next time!

Valentino Gagliardi

Valentino Gagliardi

Web Developer & IT Consultant, with over 10 year of experience, I'm here to help you developing your next idea.
Valentino Gagliardi

6 Replies to “An Introduction to Building TDD RESTful APIs with Koa 2, Mocha and Chai”

  1. I prefer do not use an ORM to create tables and migrations. When you have to create complex tables, queries, migrations normally the ORM syntax does not reach it. So, raw SQL is finally nedeed.

  2. I’ve noticed that after running the tests, the terminal hangs until you manually quit. Is there a way to kill the Koa server after the tests have completed?

Leave a Reply

Your email address will not be published. Required fields are marked *