Routing

So, the time has come for our little app to do something more than just greet the world, as glorious as that may be.

For that, we will create a basic routing system with the following considerations high up on the priority list:

  • It has to be easily extensible with more routes, authentication, authorization, and data input/output sanitizing;

  • It has to allow for easy generation of documentation;

Especially in regard to the latter, there are ridiculously powerful things like Swagger out there, and you should check them out. I, however, believe that every budding coder should learn how to think the right way to put something like that together by themselves, so as to understand at least some benefits and considerations that went into existing libraries. Thus we will be rolling our own thing here.

For starters, we will be creating a globals object, which will hold instances of our… well… everything, which we want to pass throughout the app. This is not necessarily the most efficient or elegant way to do it, but the purpose of this tutorial is to give you a very simple introspection into how architecture works. How to structure modules for more elegant solutions is way beyond the scope of this guide.

So, add the following just below app.use(router);

const globals = {
  config,
  router,
};

This will pack our configuration and the router object into the globals object, which we can easily pass everywhere. Below this declaration, add

require('./controllers/rest/router')(globals);

/controllers/rest/router/index.js will take the router property from globals and bind our routes to it, along with other relevant routing magic. Our /index.js now looks as follows:

// /index.js

const config = require('./config')();
const express = require('express');
const bodyParser = require('body-parser');

const app = express();
const router = express.Router();

router.use(bodyParser.urlencoded({ extended: true }));
router.use(bodyParser.json());
app.use(router);

const globals = {
  config,
  router,
};

require('./controllers/rest/router')(globals);

console.log(`Server listening on port ${config.infrastructure.port} in environment ${process.env.NODE_ENV} with config\n${JSON.stringify(config, 0, 2)}`);

app.listen(config.infrastructure.port);

Time to add the routes!

Create the folder controllers/rest/router/routes, and add the files index.js, user.js, and todo.js in there.

Let us first look at the structure of user.js and todo.js. Since, at this point, both look exactly the same, except that one says ‘user’ and the other ‘todo’, I will only cover one here:

// ./controllers/rest/router/routes/user.js

const Promise = require('bluebird');

const mockUser = (id, body) => (Object.assign({
  id: `${id || Math.ceil(Math.random() * 1000)}`,
  type: 'user',
  content: 'my random User',
}, body));

module.exports = globals => [
  {
    description: 'Get all users',
    method: 'get',
    route: '/user',
    cb: (params, query, body) =>
      Promise.resolve([mockUser(params.id), mockUser(params.id + 1)]),
  },
  {
    description: 'Get a user',
    method: 'get',
    route: '/user/:id',
    cb: (params, query, body) =>
      Promise.resolve([mockUser(params.id)]),
  },
  {
    description: 'Create a user',
    method: 'post',
    route: '/user',
    cb: (params, query, body) =>
      Promise.resolve([mockUser(params.id, body)]),
  },
  {
    description: 'Modify a user',
    method: 'put',
    route: '/user/:id',
    cb: (params, query, body) =>
      Promise.resolve([mockUser(params.id, body)]),
  },
  {
    description: 'Delete a user',
    method: 'delete',
    route: '/user/:id',
    cb: (params, query, body) =>
      Promise.resolve([1]),
  },
];

OK, lots of things going on here.

Up top, you can see that I defined a mockUser function, which will generate a mocked user object for us. This will not be present in the final version, and will move down the layers, as we add services, and repositories, until we are getting actual data from a database. For now, it will serve its purpose, however, and it should be self-explanatory.

Both user and todo modules export a function which takes as its argument the globals object we created in /index.js. Later on, we will have the routes get their service callbacks from globals, but, for now, it is there just to remind us of the great things to come.

When executed, the modules return an array of route objects, with several properties.

description is there in case you want to auto-generate documentation and would like a human-friendly description of each route to shine above the technical stuff.

method specifies the HTTP call type used to hit this route. It will be used when registering routes with the express router.

route is simply the route path in a format that express router can understand.

cb is the callback function that is executed when this route is hit. We will have it take the request.params, request.query, and request.body arguments, even though a GET call, for example, does not require a body, because this will allow for some nifty generalist callbacks later on. For now, just /follow. Also, everything that comes back from the callback functions will be in the form of a Promise, because either the libraries or we will make sure of that.

Now create /controllers/rest/router/routes/index.js and put the following in there:

const todo = require('./todo');
const user = require('./user');

module.exports = globals => []
  .concat(todo(globals))
  .concat(user(globals));

Easy? Easy. Should be clear, what this does.

And now, for the best part. Create /controllers/rest/router/index.js and prepare for the wild ride!

const routes = require('./routes');

const response = cb => (req, res) =>
  cb(req.params, req.query, req.body)
  .tap(r => (Array.isArray(r) && r.length === 0
    ? res.status(404)
    : res.status(200)))
  .then(r => res.json(r))
  .catch((e) => {
    res.json(e);
  });

module.exports = globals =>
  (routes(globals)
    .forEach(route =>
      globals.router[route.method](route.route, response(route.cb)))
  );

Say what?

We will use the Bluebird Promise library, because it has some nifty methods, like for example the .tap(). I will come back to that in a moment.

So, what does this file do, besides make us wish we were writing calculators again?

It exports a function, which accepts the globals object as its one and only argument. Once that function is executed, it runs a loop on the array which consists of all our route objects (remember the concatting from a minute ago?), and it registers each route with the express router.

That crazy .forEach actually does the following:

globals.router['get']('/user/:id', (params, query, body) =>
  Promise.resolve([mockUser(params.id)]))

for the route:

{
    description: 'Get a user',
    method: 'get',
    route: '/user/:id',
    cb: (params, query, body) =>
      Promise.resolve([mockUser(params.id)]),
}

Why do we not do it directly in the route file? Because DRY. Code duplication bad! Why? There are plenty of blogs around on that.

Now, what does that crazy response function do?

It has one job — to provide the middleware function for express. This means it has to return a function that takes request and response (and next and/or err) arguments. To keep it simple, we will stick with the former two.

So, the response function takes a cb — callback argument and then returns a middleware function. This middleware function conveniently has the cb conveniently accessible through a closure, which is just what we need.

The middleware function executes the cb callback function with the incoming request data params, query, and body. Once the returned promise resolves, it checks if it received something expected or unexpected and sets the response status code accordingly and then actually responds.

And what does ‘something expected or unexpected’ mean? Anything you want it to, really. In our case, everything we will be returning from lower layers will be in an array, so if the array is empty, we apparently got no hits, hence 404. If it has things in there, we return a 200. Obviously this is not nearly enough for a production-level app, but if you understand the concept here, you will be able to include and expand your own checking system.

And this is it, really. If you run the app, you will notice that you can do calls like

curl -X GET "http://localhost:8888/user"
curl -X GET "http://localhost:8888/todo/1"

and all the others specified in your routes.

So, this is it for today. Play around with the routes, see if you can add more yourself, get different outputs. And, in the next chapter, we will be adding the Application Logic Layer, a.k.a. Services.

Routing — Github Repository

results matching ""

    No results matching ""