Effective RESTful API with Koa.js
Exclusively being the backend developer during the course of CS3216 this semester, I chose Koa.js for all of my three projects. Here’s my justification for using Koa.js in my project 3’s report:
We decided to use Koa.js for the RESTful API, more specifically Koa 2, instead of Express due to native support for ES7 async/await functions. These make codes shorter, more intuitive and less error-prone. Furthermore, Koa.js is significantly more lightweight and modular, so it’s possible to install just the Koa modules we need.
Still not convinced? Check out this Quora answer:
Learn koa if:
- You generally like being ahead of the curve and on the bleeding edge
- Your project needs to be future-proof, long-lived and easy to maintain in the longer run
- You come from a programming language that has async functions, or a synchronous-style of dealing with asynchronous actions without callbacks
Who doesn’t like being ahead of the curve and on the bleeding edge? If you don’t, you’re not supposed to be taking CS3216, just saying :)
Modular APIs with Koa
After 3 projects, I have gradually refined the way to structure my project around Koa, with inspirations from MVC frameworks such as Sails.js.
In my opinions, it’s better to divide the endpoints into separated files/modules. For example, endpoints starting with /user
such as GET /user/:id
or POST /user/comment
should be grouped together in a file user.es6
.
I have seen the API codes from a few other groups, a majority of whom tend to cram all endpoints into one big file, which definitely will hinder maintenance in the long run.
Now, let’s design such a Koa.js structure with ES6/7 Node.js in a way that allows easy extensions such as Passport and JWT.
First, create a simple Koa app and initialize JWT:
import Koa from 'koa';
import BodyParser from 'koa-bodyparser';const koa = new Koa()
.use(BodyParser()); // enable JSON parsingimport passport from 'koa-passport';
koa.use(passport.initialize());import KoaJwt from 'koa-jwt';
const jwt = KoaJwt(...);
Then, we will mount available routers:
import requireDir from 'require-dir';
const routers = requireDir("./routers"); // require all router filesfor (let [key, routerClass] of Object.entries(routers)) {
routerClass = routerClass.default; // necessary for ES6 export let router = new routerClass(passport, jwt).build();
koa.use(router.routes(), router.allowedMethods());
}
In the piece of code above, every routerClass
is an instance of AbstractRouter
(implemented later), exported by a file in the ./routers
folder. In short, AbstractRouter
can be intialized with necessary objects like passport
and jwt
, which will be injected to the corresponding endpoints when the router is built. (Yes, this is a simple form of Dependency Injection).
Let’s look at how AbstractRouter
is implemented with ES6’s class
:
import Router from 'koa-router';
export default class AbstractRouter {
constructor(passport, jwt) {
this.passport = passport;
this.jwt = jwt;
} // an "abstract" method that a subclass should implement
initialize(objects) {
}
get prefix() {
return null;
} // this is called while this router is being mounted
build() {
const router = new Router({
prefix: this.prefix // prefix of all related endpoints
});
this.initialize({
router,
passport: this.passport,
jwt: this.jwt
});
return router;
}
}
With all of our preparations in place, we can finally start writing our router files. Here is an example of a /test
router, which has:
import AbstractRouter from "../AbstractRouter";
export default class extends AbstractRouter { // all of the endpoints below will start with /test
get prefix() { return "/test"; }
initialize({ router, jwt }) {
router.get('/', async(context) => {
context.body = { status: "success" };
});
router.get('/jwt', jwt, async(context) => {
if (context.state.user) {
context.body = {
"is_valid": true,
"user_id": context.state.user.id
};
} else {
context.body = {
"is_valid": false
}
}
});
}
}
As seen in the code above, it’s simple to inject the jwt
object created earlier to our endpoint /test/jwt
. With this modular structure, adding new endpoints or updating existing ones can be done in a breeze.