Skip to main content

KoaJS - Lightweight NodeJS Framework

Introduction

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

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.

Let's learn KoaJS

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.

Init the project

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

npm init -y

Then create your first file in the project:

touch src/app.js

Hello world

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

npm run dev

Congratulation! Your first Hello World with KoaJS

Basic routing

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

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

Let's refactor and re-organize the code

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

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:

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?

Simple Database Driver

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

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

API route to creating new book

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

API Testing with Postman

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

Input validation

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:

npm i yup

Create a new file called: bookInputMiddleware.js

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.

Import/Export and ES6 Syntax

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:

npm run watch && npm run dev

Exercises

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:

Create your database sample

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.

Create a REST API with the given data

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

Get all list of products

  • 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

Create a new product to the list

  • 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

Update a product with the input data

  • 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

Delete a product of a given id

  • Remove the product of id

Get one product by 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.

Help

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

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.