pkuosa-gabriel / koa-http-server
PKUOSA Web Full-Stack HW02
koa-http-server
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
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
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
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
Intall basic packages
After that, some basic packages need to be installed.
To enable hot reload, we will use nodemon.
yarn add nodemon
Then we can add a script to package.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.
# 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:
"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.
# 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:
"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.
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.
# Install koa
yarn add koa
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();
});
});
});
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
};
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
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
};
// 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.
// 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:
// 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.
# Install koa-body
yarn add koa-body
// 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.
Top comments (0)