Skip to content

KoaJS - Lightweight NodeJS Framework

After learning all the basic NodeJS/Javascript syntax and key features, it is time to move on to another level by getting to know KoaJs - a NodeJS framework. We might be familiar with ExpressJS and wondering why we - Avada team - are using KoaJS instead of ExpressJS? The answer is that: we find KoaJS is much lighter, the ecosystem is highly-decoupled with seperate module for each functionality, and you can install only when needed, nothing is redundant.

According to the homepage, KoaJS is shortly described as:

Koa is a new web framework designed by the team behind Express, which aims to be a smaller, more expressive, and more robust foundation for web applications and APIs. By leveraging async functions, Koa allows you to ditch callbacks and greatly increase error-handling. Koa does not bundle any middleware within its core, and it provides an elegant suite of methods that make writing servers fast and enjoyable.

Where to find learning resources for KoaJs

Section titled “Where to find learning resources for KoaJs”

The best place to look up for an answer with KoaJS is their documentation [here] (https://koajs.com/). However, unlike other framework, each functionality is provider by standalone package like:

Personally speaking, the best place to read documentation for KoaJS is the Readme.md of the package on Github.

It is a good practice that you, as learners, should google everything from scratch and try to self-learn the technology, which will stick for long. However, we will guide you through the first KoaJS API as a boilerplate. This will help you learn KoaJS at a faster pace.

The API that I’m going to help you set up will have some very basic features of KoaJS:

  • The API will be a REST API.
  • You will be familiar with KoaJS middleware
  • You will only interact with file as a persistent database.
  • You will learn how to organize you KoaJS project
  • You will learn how to use ES6 or newer version of ECMAScript in your project.

Create a new folder for your new project, then run the below command line to init:

Terminal window
npm init -y

Then create your first file in the project:

Terminal window
touch src/app.js

Insert this content to the app.js file:

const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(5000);

Update your package.json as below:

{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon src/app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.13.1",
"koa-body": "^4.2.0",
"koa-router": "^10.0.0"
},
"devDependencies": {
"nodemon": "^2.0.7"
}
}

Once all done, you can run this bash and then hit the localhost:5000 to see the Hello world

Terminal window
npm run dev

Congratulation! Your first Hello World with KoaJS

In order to perform routing, we need to use koa-router, which we installed in the first place. You should read the README.md on Github to understand the basic syntax of the KoaJS First, let’s me follow the basic setup of the koa-router. Create a routes.js file by this bash

Terminal window
touch src/routes/routes.js

Then fill the content of the file with:

const Router = require('koa-router');
// Prefix all routes with /books
const router = new Router({
prefix: '/api'
});
const books = [
{id: 101, name: 'Fight Club', author: 'Chuck Palahniuk'},
{id: 102, name: 'Sharp Objects', author: 'Gillian Flynn'},
{id: 103, name: 'Frankenstein', author: 'Mary Shelley'},
{id: 104, name: 'Into The Willd', author: 'Jon Krakauer'}
];
// Routes will go here
router.get('/books', (ctx) => {
ctx.body = {
data: books
};
});
router.get('/books/:id', (ctx) => {
try {
const {id} = ctx.params;
const getCurrentBook = books.find(book => book.id === parseInt(id));
if (getCurrentBook) {
return ctx.body = {
data: getCurrentBook
}
}
ctx.status = 404;
return ctx.body = {
status: 'error!',
message: 'Book Not Found with that id!'
};
} catch (e) {
return ctx.body = {
success: false,
error: e.message
}
}
});
module.exports = router;

Then, update the app.js file like this:

const Koa = require('koa');
const koaBody = require('koa-body');
const routes = require('./routes/routes.js');
const app = new Koa();
app.use(koaBody());
app.use(routes.routes());
app.use(routes.allowedMethods());
app.listen(5000);

Go to two routes set up by the API:

In order to begin, we will not connect to any database, we will return the data for the API by predefined data (like the books variable). The API just simply contains two routes:

  • /api/books
  • /api/books/:id

We will extract all the handlers of each routes to a file called bookHandler.js

Terminal window
touch src/handlers/books/bookHandlers.js
const {getAll: getAllBooks, getOne: getOneBook} = require("../../database/bookRepository");
/**
*
* @param ctx
* @returns {Promise<void>}
*/
async function getBooks(ctx) {
try {
const books = getAllBooks();
ctx.body = {
data: books
};
} catch (e) {
ctx.status = 404;
ctx.body = {
success: false,
data: [],
error: e.message
};
}
}
/**
*
* @param ctx
* @returns {Promise<{data: {author: string, name: string, id: number}}|{success: boolean, error: *}|{message: string, status: string}>}
*/
async function getBook(ctx) {
try {
const {id} = ctx.params;
const getCurrentBook = getOneBook(id);
if (getCurrentBook) {
return ctx.body = {
data: getCurrentBook
}
}
throw new Error('Book Not Found with that id!')
} catch (e) {
ctx.status = 404;
return ctx.body = {
success: false,
error: e.message
}
}
}
module.exports = {
getBooks,
getBook
};

Then, we will migrate the database logic to a file called bookRepository.js:

Terminal window
touch src/database/bookRepository.js
const books = [
{id: 101, name: 'Fight Club', author: 'Chuck Palahniuk'},
{id: 102, name: 'Sharp Objects', author: 'Gillian Flynn'},
{id: 103, name: 'Frankenstein', author: 'Mary Shelley'},
{id: 104, name: 'Into The Willd', author: 'Jon Krakauer'}
];
/**
*
* @returns {[{author: string, name: string, id: number}, {author: string, name: string, id: number}, {author: string, name: string, id: number}, {author: string, name: string, id: number}]}
*/
function getAll() {
return books;
}
/**
*
* @param id
* @returns {{author: string, name: string, id: number} | {author: string, name: string, id: number} | {author: string, name: string, id: number} | {author: string, name: string, id: number}}
*/
function getOne(id) {
return books.find(book => book.id === parseInt(id));
}
module.exports = {
getOne,
getAll
};

Finally, update the routes.js file:

const Router = require('koa-router');
const bookHandler = require('../handlers/books/bookHandlers');
// Prefix all routes with /books
const router = new Router({
prefix: '/api'
});
// Routes will go here
router.get('/books', bookHandler.getBooks);
router.get('/books/:id', bookHandler.getBook);
module.exports = router;

After refactoring, you can see the your first KoaJS API seems a bit more organized. Ready to go further?

In this example, because we will not use any database technology, we will use a persistent storage solution, which is built-in in any system - File. We will store the books list in the file called: books.json

Terminal window
touch src/database/books.json
{
"data": [
{
"id": 101,
"name": "Fight Club",
"author": "Chuck Palahniuk"
},
{
"id": 102,
"name": "Sharp Objects",
"author": "Gillian Flynn"
},
{
"id": 103,
"name": "Frankenstein",
"author": "Mary Shelley"
},
{
"id": 104,
"name": "Into The Willd",
"author": "Jon Krakauer"
}
]
}

Then you update the repository logic again to add some logic:

  • Get all books
  • Get one book by id
  • Add new book the the list
const fs = require('fs');
const {data: books} = require('./books.json');
/**
*
* @returns {[{author: string, name: string, id: number}, {author: string, name: string, id: number}, {author: string, name: string, id: number}, {author: string, name: string, id: number}]}
*/
function getAll() {
return books
}
/**
*
* @param id
* @returns {{author: string, name: string, id: number} | {author: string, name: string, id: number} | {author: string, name: string, id: number} | {author: string, name: string, id: number}}
*/
function getOne(id) {
return books.find(book => book.id === parseInt(id));
}
/**
*
* @param data
*/
function add(data) {
const updatedBooks = [data, ...books];
return fs.writeFileSync('./src/database/books.json', JSON.stringify({
data: updatedBooks
}));
}
module.exports = {
getOne,
getAll,
add
};

I will help you create an API route to create a new book in the list. This routes will have:

  • HTTP method: POST
  • Path: /api/books
  • Input validation

In order to add a new route, we register a new route in the routes.js file:

router.post('/books', bookHandler.save);

For this route to work, just the same as the two above routes, we need a handler to handle the HTTP request. Update your bookHandler.js this function:

const {getAll: getAllBooks, getOne: getOneBook, add: addBook} = require("../../database/bookRepository");
/**
*
* @param ctx
* @returns {Promise<void>}
*/
async function getBooks(ctx) {
try {
const books = getAllBooks();
ctx.body = {
data: books
};
} catch (e) {
ctx.status = 404;
ctx.body = {
success: false,
data: [],
error: e.message
};
}
}
/**
*
* @param ctx
* @returns {Promise<{data: {author: string, name: string, id: number}}|{success: boolean, error: *}|{message: string, status: string}>}
*/
async function getBook(ctx) {
try {
const {id} = ctx.params;
const getCurrentBook = getOneBook(id);
if (getCurrentBook) {
return ctx.body = {
data: getCurrentBook
}
}
throw new Error('Book Not Found with that id!')
} catch (e) {
ctx.status = 404;
return ctx.body = {
success: false,
error: e.message
}
}
}
/**
*
* @param ctx
* @returns {Promise<{success: boolean, error: *}|{success: boolean}>}
*/
async function save(ctx) {
try {
const postData = ctx.request.body;
addBook(postData);
ctx.status = 201;
return ctx.body = {
success: true
}
} catch (e) {
return ctx.body = {
success: false,
error: e.message
}
}
}
module.exports = {
getBooks,
getBook,
save
};

One of the most famous tool for API testing is Postman, you can find a light-weight version for Chrome here. Once you install it, please see this screencast for guidance on how to use Postman to test against your KoaJS API

You may input wrong input at your end, maybe it is not JSON, or it is not formatted correctly, maybe missing name like the one below:

{
"id": 155,
"author": "John Doe"
}

Then you need a way to make sure that you can validate the API input. In order to achieve that, you will use yup.

Yup is a JavaScript schema builder for value parsing and validation. Define a schema, transform a value to match, validate the shape of an existing value, or both. Yup schema are extremely expressive and allow modeling complex, interdependent validations, or value transformations.

Install yup by running this command:

Terminal window
npm i yup

Create a new file called: bookInputMiddleware.js

Terminal window
touch src/middleware/bookInputMiddleware.js
const yup = require('yup');
async function bookInputMiddleware(ctx, next) {
try {
const postData = ctx.request.body;
let schema = yup.object().shape({
id: yup.number().positive().integer().required(),
name: yup.string().required(),
author: yup.string().required()
});
await schema.validate(postData);
next();
} catch (e) {
ctx.status = 400;
ctx.body = {
success: false,
errors: e.errors,
errorName: e.name
}
}
}
module.exports = bookInputMiddleware;

In order to register this middleware, we update the routes.js file:

router.post('/books', bookInputMiddleware, bookHandler.save);

As you can see, in order to apply a middleware, we just need to register it right before the handler.

Now, try again with invalid data format by Postman.

The working code of the sample API can be found at https://gitlab.com/anhnt7/training-code-sample. You can clone or download this. You should see that this version has import/export used over the require keyword. Compare the difference and ask yourself whether you like the import/export or the require.

Since now, you will use this new boilerplate of import/export syntax of KoaJs API application. In order to run the application, you should run the review command:

Terminal window
npm run watch && npm run dev

With the sample KoaJS API given above, you should be able to develop a functional RESTful KoaJS API. Follow the below requirements and develop the requested API:

Just like the books.json file as your database, you will need to generate a file called products.json contains a list a products up to 1000 records with format like this:

[
{
"id": 1,
"name": "Product name here",
"price": 10,
"description": "Description here",
"product": "Product type here",
"color": "Color here",
"createdAt": "Date here",
"image": "Image URL"
}
]

You should use the faker.js module to generate the above data and NodeJS fs module write to file. Notice the namespace like ecommerce and random may serve your purpose.

With data given above, design a REST API with all resource routes described as below:

MethodRouteDescriptionParameter
GET/api/productsGet all list of productslimit, orderBy
POST/api/productsCreate a new product to the listNone
PUT/api/product/:idUpdate a product with the input dataNone
DELETE/api/product/:idDelete a product of a given idNone
GET/api/product/:idGet one product by IDfields
  • The API should return all the list of products in file products.json.
  • The API should have the parameter limit with the request URL like /api/product?limit=5 then return only 5 first products
  • The API should have the parameter limit with the request URL like /api/product?sort=desc or /api/product?sort=asc then return products order by createdAt field desc or asc
  • Write a middleware to validate JSON format of the input
  • You should send an JSON request to this API and add a new record. The createdAt will be the value of the submit.
  • Use yup to validate input format
  • Write a middleware to validate JSON format of the input
  • The API should receive the JSON request to the API, merge the updated data to the object with the given ID.
  • Use yup to validate input format
  • Remove the product of id

The API should have the parameter fields with the request URL like /api/product?fields=name,price then return only picked fields of the product.

If you need any help clarifying these requirements, see this video:

Create a HTML page, render every product list or one product

Section titled “Create a HTML page, render every product list or one product”

You can use koa-views or koa-ejs to render a HTML page of the given data. The page should be at /products. You will need to learn how to render HTML view with KoaJS on your own to practice your research skill.