A Modular Approach to create APIs using Express.js and Node.js – Sandny Blog

November 05, 2017 0 Comments

A Modular Approach to create APIs using Express.js and Node.js – Sandny Blog

 

 

In this post, I would like to share a modular architecture for APIs created with express.js. The modular approach which discussed here is having a module which can perform each and every task relevant to the API. It has following features.

  1. The functionality is well defined.
  2. Modules are separated context wise and functionality wise. They are entities which can be treated individually which make them modular.
  3. Every module poses similar behaviours. So each module has a similar footprint to invoke methods and provide service.

If you find these kinds of features it would be appropriate to come up with a modular architecture. It would provide following advantages

  1. All have the same footprint and therefore it would be easy to develop and introduce new features.
  2. Can alter the underlying architecture once and provide the change to each module easily.
  3. The module integration is easy and therefore it will save time when developing new features.

But there are some cons too.

  1. Since all the module footprints are the same, it would have a high coherence to the architecture itself. If you need a functionality that cannot be provided with the architecture, you will have to introduce it in a new definition. But if you come into a such a situation, I think there could be a solution as I didn’t find such a case when I was developing.
  2. If you want to change some intermediate service or a middleware, it would affect all the modules. So be careful when coding the underlying architecture and look towards any unwanted repetitive function invocations.

That being said let’s get our fingers working. Most of the APIs defined in express has following features,

  • A router with the paths and http request methods.
  • A schema which can validate request body before forwarding towards next middleware.
  • Handler functions to treat the request and generate a response.

These features are well defined, modular and have similarities. So we can use this to create a module. A module would first be a folder containing module footprints. So our folder structure should contain modules. I’m going to show you a todo example in this post.

-/ src -/ initializer/ -/ modules - / todo - / Handler.js   - / Router.js - / Schema.js - / module2 - / module3

Each module contains three files to add defined functionalities. My application is a simple todo app and the idea is all the todo related functionalities are driven by this API. This contains GET_TODO, GET_ALL_TODOS, POST_TODO and DELETE_TODO http requests. I have named the key variables as event types which could happen in the todo app context. So I will define the path as follows in Router.js.

export const API_EVENTS = { POST_TODO: 'POST_TODO', GET_TODO: 'GET_TODO', GET_ALL_TODOS: 'GET_ALL_TODOS', DELETE_TODO: 'DELETE_TODO', }; export const todoAPI = { [API_EVENTS.POST_TODO]: { method: 'POST', path: '/' }, [API_EVENTS.GET_TODO]: { method: 'GET', path: '/:id' }, [API_EVENTS.GET_ALL_TODOS]: { method: 'GET', path: '/' }, [API_EVENTS.DELETE_TODO]: { method: 'DELETE', path: '/:id' }, }; 

And I have a schema to validate the POST_TODO event. I have used Joi library to validate the http body attributes. Following would be the code in our Schema.js file.

import Joi from 'joi'; const postTodo = Joi.object().keys({ todo: Joi.string().alphanum().min(3).max(30).required(), timestamp: Joi.number(), }); const schema = { POST_TODO: postTodo, } export default schema; 

Then someplace to handle the events requested from the router. I will have the handler to the API_EVENTS defined in the router. I have defined an array of todos so that I could store and delete the todos.

import { API_EVENTS } from './Router'; let todos = []; export const handler = { [API_EVENTS.GET_ALL_TODOS]: (req, res, next) => { res.send(JSON.stringify(todos)); }, [API_EVENTS.POST_TODO]: (req, res, next) => { const { todo, timestamp } = req.body; todos.push({ todo, timestamp }) res.send('OK'); }, [API_EVENTS.GET_TODO]: (req, res, next) => { res.send(JSON.stringify(todos[req.params.id])); }, [API_EVENTS.DELETE_TODO]: (req, res, next) => { const deleteId = req.params.id; if (deleteId && todos[req.params.id]) { todos.splice(deleteId, 1); res.send('OK'); } else { res.send('BAD'); } }, }; export default handler; 

How is that?  You have a full defined API with above code lines. And you can customize anything as well. Now, how did we do this? We haven’t imported fancy stuff to connect these code lines. That is what modular structure is all about. We should be able to keep what is necessary and do the connections somewhere else. I have created a package called initializer to initialize all the routes and add it into the express context.

First one to go would be the app.js where all the app is created and all the others are combined. I have included the hello at the root path just to for you to do anything you want with it.

import { connectRouters, express } from './initializer/framework'; import bodyParser from 'body-parser'; const app = express(); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json({ type: 'application/json' })); connectRouters(app); app.get('/', function (req, res) { res.send('Hello from Todo API') }); app.listen(3000, function () { console.log('TODO API app listening on port 3000!') }); 

Afterwards, we need to construct this connectRouters method since it’s the place where we bind all the routers, handlers and schemas. Before going towards that, we will first try to aggregate the individual module functionalities. I have created three files called routers.js, schemas.js and handlers.js inside initializer package and I have used lodash to create the data structure I want. And a file called appModules.js so that I can define where my module is.

// appModule.js export const modules = [ 'todo', ]; export default modules; 
import modules from './appModules'; import _ from 'lodash'; // routers.js export const routers = _(modules) .mapKeys(module => module) .mapValues((routerName) => { try { return require(`../modules/${routerName}/Router`).default; } catch (error) { console.log(error); throw 'Router names are not configured properly'; } }).value(); export default routers;
import appModules from './appModules'; import _ from 'lodash'; // schemas.js export const schemas = _(appModules) .mapKeys(module => module) .mapValues((module) => { try { return require(`../modules/${module}/Schema`).default; } catch (error) { console.log(error); throw 'Schema names are not configured properly'; } }).value(); export default schemas;
import appModules from './appModules'; import _ from 'lodash'; //handlers.js export const handlers = _(appModules) .mapKeys(module => module) .mapValues((module) => { try { return require(`../modules/${module}/Handler`).default; } catch (error) { console.log(error); throw 'Handler names are not configured properly'; } }).value(); export default handlers; 

We can see some repetition here but its better if you can keep this files separately as in future you will need to complicate these files in a different way to support the features you need.

Then you will need to create the framework.js file to combine these three files. I have included the whole file I coded. And please do note that this is not complete and you will need to add more beauty to the code if you intend to use this.

import express from 'express'; import Joi from 'joi'; import _ from 'lodash'; import modules from './appModules'; import schemas from './schemas'; import handlers from './handlers'; import routers from './routers'; const createRouter = () => (routingContext) => { const router = express.Router(); }; const validatorMiddleware = (schema) => (req, res, next) => { if (_.isNull(schema)) { console.log('The schema is null') next(); } else { const result = Joi.validate(req.body, schema); console.log('Req Body: ', req.body); result.error === null ? next() : res.status(422).json({ errors: result.error}); console.log(result.error); } }; const getRouterPath = (moduleName) => `/${moduleName}`; const defaulHandler = (req, res) => { res.status(404).json({ errors: ' Not Implemented'}); }; const connectRouters = (app) => { console.log('connecting Routers', JSON.stringify(routers)); _.forEach(modules, (moduleName) => { const router = express.Router(); const moduleSchema = schemas[moduleName]; const moduleHandler = handlers[moduleName]; console.log(JSON.stringify(handlers)); _.forEach(routers[moduleName], (api, apiKey ) => { const { path, method } = api; const schema = _.isNil(moduleSchema[apiKey]) ? null : moduleSchema[apiKey]; const handler = _.isNil(moduleHandler[apiKey]) ? defaulHandler : moduleHandler[apiKey]; // connection router[_.lowerCase(method)](path, validatorMiddleware(schema), handler); }); app.use(getRouterPath(moduleName), router); }); }; export { _, createRouter, express, validatorMiddleware, connectRouters, } 

In the connectRouters function, the modules are iterated and all the handlers, routers and schemas are connected. You can include more middlewares when you are making the connection with the router. And there are simpler ways to code this and you will have to define how to use it. And also this was coded in the way I described in the post and you could try it too.
I don’t like the data structure I created for storing the modules. If you could make it flatter and 1 level deep that would be awesome. But it is a one-time initialization and there is no harm in it.

The github repo for this tutorial is in https://github.com/sandaruny/moduler-express-api

Dont forget to add your feedback on this coz I want to know what could go wrong with this.


Tag cloud