Test-driven development of an HTTP server with Koa.js
Tags: node, http, backend, javascript
Published: 2019-01-03 09:14:38

---
title: Test-driven development of an HTTP server with Koa.js
published: true
description: 
tags: node,http,backend,javascript
---

{% github https://github.com/pkuosa-gabriel/koa-http-server %}

## Aim of this project

This project is aimed at implementing a simple http server using [koa.js](https://github.com/koajs/koa). In _The Node Beginner Book_, a similar server was implemented without any frameworks.

In the following sections, the development of this project is illustrated step by step.

## Initialization

### Dependencies

First, all basic dependencies need to be installed, e.g., node, npm (or yarn). As I am using MacOS, I have installed all the prerequisites via homebrew:

```bash
# Install node and yarn
# If you want to use npm, you can install node only, which includes npm
brew install node yarn
```

I personally prefer yarn to npm as the package manager. If you want to use npm, that is definitely OK.

If you want to switch between different node versions, you can install nvm via brew, and then install different node versions via nvm.

```bash
# Install nvm
brew install nvm

# Install different node versions
nvm install 10
nvm install 8

# Select a version to use
nvm use 10
```

Now you have both node 8 and node 10 installed, while node 10 is being used in the current environment.

### Repository initialization

Next, it is time to init a repository. There are numerous scaffolds available, but we are going to build this project from scratch, so no scaffolds will be used.

```bash
# Create project directory
mkdir koa-http-server
cd koa-http-server

# Initialize git
git init

# Initialize package.json
yarn init

# Create .gitignore
touch .gitignore

# Create the entrypoint
touch index.js
```

Note that `yarn init` will ask you a series of questions interactively, you can just answer them as you like.

To ignore unrelated files, you can use the following `.gitignore` file as a template, and add or modify anything in it as you want.

```txt
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next

# IDEs
.vscode
.idea

# public
public/*
!public/favicon.ico
```

### Intall basic packages

After that, some basic packages need to be installed.

To enable hot reload, we will use nodemon.

```bash
yarn add nodemon
```

Then we can add a script to `package.json`

```json
"main": "index.js",
"scripts": {
    "dev": "nodemon --watch"
}
```

Note that we do not need to specify `index.js` in the script, for it has been defined in `"main"`. If you did not specify the entrypoint file during `yarn init`, then you should specify it in the script.

We are going to follow BDD (Behavior-Driven-Development) in this project. We will use Mocha+Chai as the test framework. These packages should be installed as dev-dependencies. Also, we will use Istanbul to count code coverage.

```bash
# Install test-related packages as dev dependencies
yarn add mocha chai chai-http nyc --dev

# Create a subfolder for tests
mkdir test

# Create our first test file
touch test/index.spec.js
```

And then the corresponding scripts:

```json
"scripts": {
    "coverage": "nyc report --reporter=json",
    "test": "nyc mocha test/*.js"
}
```

We always want our code to be clean and neat. For this purpose, ESLint is the best choice.

```bash
# Install ESLint as a dev dependency
yarn add eslint --dev

# Interactively configure your rules
node_modules/eslint/bin/eslint.js --init
```

After that, we can add one more script:

```json
"scripts": {
    "lint": "eslint *.js test/*.js --fix"
}
```

`--fix` is used so that style errors will be automatically fixed when we run `yarn lint`.

To enable ESLint in mocha environment, we need to modify the generated ESLint configuration file (`.eslintrc.yml` in my case) manually.

```yaml
env:
  es6: true
  node: true
  mocha: true
```

Now we have finished most configurations. In my project, I have also configured codebeat, renovate, codecov, mergify, travis and heroku, so as to empower a full-featured CI/CD flow. These details will not be discussed in this note, but you can refer to the code, or search and read the documentation of each tool mentioned above.

## Start a server

As we are going to use the koa framework, we should install the package first.

```bash
# Install koa
yarn add koa
```

We will write the test first.

```javascript
// test/index.spec.js

const chai = require("chai");
const chaiHttp = require("chai-http");
const { server } = require("../index");
const expect = chai.expect;

chai.use(chaiHttp);

describe("Basic routes", () => {
  after(() => {
    server.close();
  });

  it("should get HOME", done => {
    chai
      .request(server)
      .get("/")
      .end((err, res) => {
        expect(res).to.have.status(200);
        expect(res.text).equal("Hello World");
        done();
      });
  });
});
```

We can then run `yarn test` , and it will fail doubtlessly for we have not implemented the corresponding functions. We are going to do it now.

```javascript
// index.js

const Koa = require("koa");
const app = new Koa();

app.use(async ctx => {
  ctx.body = "Hello World";
});

const server = app.listen(3000);

module.exports = {
  server
};
```

Now we can run `yarn test` again. The test should pass, and the coverage should be 100%. Hurray!

## Use routers

An http-server cannot be just a 'Hello World' caster. Different routes are needed to offer different contents.

```bash
# Create a file to save all the routes
touch router.js
```

### Migrate existing code

We will first migrate the 'Hello World' code to `router.js` while not letting the test fail.

```javascript
// router.js

const router = require("koa-router")();

const route = router.get("home", "/", home);

async function home(ctx) {
  ctx.body = "Hello World";
}

module.exports = {
  route
};
```

```javascript
// index.js

const Koa = require("koa");
const { route } = require("./router");

const app = new Koa();

app.use(route.routes());

const server = app.listen(3000);

module.exports = {
  server
};
```

Now the route '/' is defined in `router.js`, and the test should still pass.

### Add new routes

The 'POST /upload/text' route is discussed here as an example.

Test goes first.

```javascript
// test/index.spec.js

// ...
it("should upload a text", done => {
  chai
    .request(server)
    .post("/upload/text")
    .set("content-type", "application/json")
    .send({ textLayout: "hello" })
    .end((err, res) => {
      expect(res).to.have.status(200);
      expect(res.text).equal("You've sent the text: hello");
      done();
    });
});

// ...
```

Then the implementation:

```javascript
// router.js

const route = router
  .get("home", "/", home)
  .post("upload-text", "/upload/text", uploadText);

// ...

async function uploadText(ctx) {
  const text = ctx.request.body.textLayout;
  ctx.body = `You've sent the text: ${text}`;
}

// ...
```

However, test will fail!

The reason is that a body-parser is needed so that chai-http can work fluently. Here, we will use koa-body, because it supports multipart.

```bash
# Install koa-body
yarn add koa-body
```

```javascript
// index.js

// ...

const koaBody = require("koa-body");

// ...

app.use(koaBody());
app.use(route.routes());

// ...
```

The test shall pass now. Congratulations!

## Render pages

koa-ejs is used for rendering. Details can be seen in the code.

## Upload files

Details can be seen in the code.

## Acknowledgement

I must thank PKUOSA for offering such a precious chance for me to learn, practice and strengthen web development skills.