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:
Method | Route | Description | Parameter |
---|---|---|---|
GET | /api/products | Get all list of products | limit, orderBy |
POST | /api/products | Create a new product to the list | None |
PUT | /api/product/:id | Update a product with the input data | None |
DELETE | /api/product/:id | Delete a product of a given id | None |
GET | /api/product/:id | Get one product by ID | fields |
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 bycreatedAt
fielddesc
orasc
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.