Test-driven development of an HTTP server with Koa.js

lucifer1004

Gabriel Wu

Posted on January 3, 2019

Test-driven development of an HTTP server with Koa.js

GitHub logo pkuosa-gabriel / koa-http-server

PKUOSA Web Full-Stack HW02

koa-http-server

License: MIT codecov codebeat badge

A simple http server based on koa.js. A toy version is deployed on Heroku, feel free to have a try.

Notes can be found here.

If you have any questions or suggestions, just send me an e-mail. If you find any bugs, please create an issue in this repository. Pull requests are also welcome.




Aim of this project

This project is aimed at implementing a simple http server using koa.js. 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:

# Install node and yarn
# If you want to use npm, you can install node only, which includes npm
brew install node yarn
Enter fullscreen mode Exit fullscreen mode

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.

# Install nvm
brew install nvm

# Install different node versions
nvm install 10
nvm install 8

# Select a version to use
nvm use 10
Enter fullscreen mode Exit fullscreen mode

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.

# 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
Enter fullscreen mode Exit fullscreen mode

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.

# 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
Enter fullscreen mode Exit fullscreen mode

Intall basic packages

After that, some basic packages need to be installed.

To enable hot reload, we will use nodemon.

yarn add nodemon
Enter fullscreen mode Exit fullscreen mode

Then we can add a script to package.json

"main": "index.js",
"scripts": {
    "dev": "nodemon --watch"
}
Enter fullscreen mode Exit fullscreen mode

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.

# 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
Enter fullscreen mode Exit fullscreen mode

And then the corresponding scripts:

"scripts": {
    "coverage": "nyc report --reporter=json",
    "test": "nyc mocha test/*.js"
}
Enter fullscreen mode Exit fullscreen mode

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

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

# Interactively configure your rules
node_modules/eslint/bin/eslint.js --init
Enter fullscreen mode Exit fullscreen mode

After that, we can add one more script:

"scripts": {
    "lint": "eslint *.js test/*.js --fix"
}
Enter fullscreen mode Exit fullscreen mode

--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.

env:
  es6: true
  node: true
  mocha: true
Enter fullscreen mode Exit fullscreen mode

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.

# Install koa
yarn add koa
Enter fullscreen mode Exit fullscreen mode

We will write the test first.

// 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();
      });
  });
});
Enter fullscreen mode Exit fullscreen mode

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.

// 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
};
Enter fullscreen mode Exit fullscreen mode

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.

# Create a file to save all the routes
touch router.js
Enter fullscreen mode Exit fullscreen mode

Migrate existing code

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

// router.js

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

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

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

module.exports = {
  route
};
Enter fullscreen mode Exit fullscreen mode
// 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
};
Enter fullscreen mode Exit fullscreen mode

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.

// 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();
    });
});

// ...
Enter fullscreen mode Exit fullscreen mode

Then the implementation:

// 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}`;
}

// ...
Enter fullscreen mode Exit fullscreen mode

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.

# Install koa-body
yarn add koa-body
Enter fullscreen mode Exit fullscreen mode
// index.js

// ...

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

// ...

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

// ...
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
lucifer1004
Gabriel Wu

Posted on January 3, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related