ZarahioN Presents

Answering why

Routing (Ko)Koa

So, we’ve figured out how (Ko)Koa middleware should work, right-o? I think so, at the very least.

And.. thinking about it I finally understand why it was so strange to see empty server.js file and wonder what am I missing? Thing is – I’ve been working with either WP, which is basic PHP, or Nuxt completely serverless SPAs (where only thing we need server for is pre-rendering bunch of pages)

So, surprisingly I haven’t been working with either express or any Node servers for quite some time (not counting strange adventure into firebase functions-supported monstrocity)

And here I am, staring on mostly empty blanket of our “server”. Well, what are node servers usually are for? Erm…
Right! REST apis (or whatever really, I’m not exactly sure)

And, surprisingly enough, we got a stale dump frontend project on Nuxt that I’ve even bundled with blank (Ko)Koa server from the get go. But, we won’t be delving into Nuxt and Vue topics here and for short while will just run two servers on separe ports, joing forces once we are done learning how to do most basic stuff with just (Ko)Koa.

Since we are now going to build API for “some other” “already built” application, we’ll have to switch a subject a little bit from generic commerce application to media-oriented community platform with addition of selling merchandize (cause I’ve been postponing payment integration for long enough)

And with this goal vanishing from mind, here thee go!

(Ko)koa-router

yarn add koa-router as ussl

Yet, we have to think just a lil more, I know it’s hard, but we have to persevere. So, we’re going to build an API, and we’ll (in future yas) be serving it alongside public app, i.e. from that same domain, so it does have a little bit of sense to futureproof our efforts by nesting our future access points. Which means to give out our precious data not just on example.com/gimme/all/data but on example.com/api/v0/gimme/all/data. Since we’ll be able:

  1. as I’ve said, to not pollute public scope so much,
  2. and to make drastic changes in api, name it v+1 and abandon vCURRENT ship without telling anyone and instantly breaking old boys.

Which (Ko)koa-router allows via several routes:

  1. Direct use-ing:
var api = new Router();
var routes = new Router();

api.get('/', (ctx, next) => {...});
api.get('/titles', (ctx, next) => {...});
routes.use('/api/:api_ver', api.routes(), api.allowedMethods());

// responds to "/api/v0" and "/api/v2/titles"
app.use(routes.routes());
  1. Prefixing:
var api = new Router({
  prefix: '/api/v0'
});

forums.get('/', ...); // responds to "/api/v0"
forums.get('/titles', ...); // responds to "/api/v0/titles"

I do like both of them, tbh, but former looks too complicated for our simple purpose of preventing easy access to API. But it should provide to be of utmost help if we’ll decide to create custom taxonomy hierarchy or likes.

For example consider two routes /cats/mein-kun/37-steven and /dogs/husky/67-mike where cats, mein-kun, dogs and husky all can be custom taxonomies with several levels of hierarchy. Cool jazz, cool

So, I’ll guesstimate we kinda ready to start, only 56 lines in:

const Router = require('koa-router');

const url = require('url');

let app = new Koa();

let api = new Router({
    prefix: '/api/v0'
});

api.get('/', async (ctx, next) => {
    ctx.body = {
        'what would we return?': false
    }
    await next();
});
api.use(async (ctx, next) => {
    await next();
    ctx.body = JSON.stringify(ctx.body, null, 2);
})

app
    .use(api.routes())
    .use(api.allowedMethods())

But you know I was just kidding, more thinking! Get me the drawing board!

As you should have noticed I’ve instantiated Router with prefix /api/v0 which obviously makes any requests to anything other that localhost/api/v0/* return 404. But, I still haven’t decided what to return and from where, which points us into deciding – WTF are we going to create. That scary question that requires to stop punching balls and start deciding, or at least drawing:

Which brings us 3 base models, one of which isn’t neccesarilly important right now and kinda has to be flushed out, but we’ll leave it as is until we need it. So, in terms of nice and dandy REST we (I think so?..) need few HTTP methods to support each future model: GET and POST at the very least, and POST + part of user’s GET should be protected.. ya… That’s a problem, but as I’ve said thee perserver or thee dee, or something like that.

So we should punch kb some more and produce the likes:

const BP = require('koa-bodyparser');
api.use(BP());

api.use(async (ctx, next) => {
    await next();
    ctx.body = JSON.stringify(ctx.body, null, 2);
})

let users = [{
    name: 'Pipik',
    role: 'supa-pupa',
    _accessLevel: 99,
},{
    name: 'Nyar',
    role: 'voicer',
    _accessLevel: 3,
    social: {
        vk: 'https://vk.com/nyanyar'
    }
}]

api.get('/user', async (ctx, next) => {
    ctx.body = users;
    await next();
})

api.get('/user/:id', async (ctx, next) => {
    let user = users[ctx.params.id];
    if (user)
        ctx.body = user;
    await next();
})

api.post('/user', async (ctx, next) => {
    // body should be parsed by bodeparser we've attached earlier
    let data = ctx.request.body;
    if (data.name !== '') {
        users.push({
            ...data,
            _accessLevel: -1,
        });
        ctx.body = users.slice(-1)[0];
    }
    await next();
})

Remember I’ve said some smarty stuff about securing users and etc? Nah, not going to happen this time. We’ll do that once passport is inbound though, but first we need to create and set up proper DB access, not like I wouldn’t try to store all our data in memory, but that usually causes unnecessary problems.

BS aside, what do we have in our code? Pretty simple stuff:
1-2: require and use of bodyparser, which will transform JSON post body into simple object that we can utilize,
4-7: our little JSON formatter,
9-20: our “data structure” with sample data, this one will go outside first since it’s useless to have RAM data storage without persistence in our case
22-25, 27-32, 34-45: our basic CR~UD~, it’s of utmost simplicity since we’re not going to let it stay for long either. Main point of current endpoints is to serve something live when we query them. But (and a big Butt must say) their simplicity won’t change much – we’ll return all users on GET: /api/v0/user and we’ll add a user on POST: /api/v0/user, etc. We’ll spyce some security and checks in there, of course, but gist should stay same.

By the way, take closer look at our JSON formatter, notice that it’s use-d almost first in our middleware chain, and notice its await next() as first line – it makes it wait until Koa goes through whole chain, gets response on our endpoint and starts to go back hitting our formatter and bodyparser second time and then exiting out of router.

Such bi-directional call stack gives us interesting ways to manipulate and observe data in Koa or place our logic throughout execution time. I would extrapolate that such behaviour wishes for error catching and logging solutions out of all things that you can do with it.

Anyway! We’ll finish our routes for titles in similar manner and finish up with this BS cause I do want to take a look at mongoose which I, by being a monk, was evading for quite some time already.

So in so, gotsa see thee later. And while waiting, you can check out complete codebase.

Leave a Reply

Your email address will not be published. Required fields are marked *