A Modular Approach to create API using Express.js and…
In this post, I would like to share a Modular Approach to create API generated 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.
Features of Modular Approach to create API
- The functionality is well defined.
- Modules are separated context wise and functionality wise. They are entities which can be treated individually which make them modular.
- 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
- All have the same footprint and therefore it would be easy to develop and introduce new features.
- Can alter the underlying architecture once and provide the change to each module easily.
- The module integration is easy and therefore it will save time when developing new features.
But there are some cons too.
- 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.
- 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.
Hands on code!
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 approach to create API 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.
How to do it
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.
The Framework that holds these codes
After all 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 just came up with idea modular approach to create API coded it.
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.
21 COMMENTS
This approach reminds me a lot of some similar approaches I was making when I wanted to leverage Swagger definitions to provide validations and methods to my “modules”. The one thing that it did though was tightly couple my stuff to REST and Swagger. When we started discussing implementing GraphQL, all of a sudden we were trying to figure out if GraphQL should be calling our REST endpoints or vice versa. That seems inefficient. “REST” and “GraphQL” are view layers and probably shouldn’t be in the business logic. I’ve started rediscovering MVC, and realizing most of my code should be just function calls that take care of data validation, authorizing (different from authenticating), and database interactions, that way, tying it to Express, or Koa, or GraphQL, or {insert new hotness} is really just translating inputs to function calls. The more you depend on your view layer for business logic, the faster your code will become legacy code
You are exactly right and thanks for pointing it out! I also thought of that and added it as an con of this architecture. But at a point I realized that if we are using the middleware structure, this could come handy, although it would be troublesome for the huge chunk of code which does all of the work. I think this approach will be appropriate when the dependencies becomes more modular like. Perhaps for microservice or a proxy. Yet again its {insert new hotness} tag as you’ve mention.
Simply want to say your article is as amazing. The clearness to your put up is just cool and i could assume you’re an expert in this subject. Fine along with your permission let me to grasp your RSS feed to stay up to date with drawing close post. Thanks 1,000,000 and please carry on the rewarding work.
Thanks for the suggestions you have shared here. Yet another thing I would like to talk about is that laptop memory demands generally go up along with other innovations in the technological innovation. For instance, any time new generations of cpus are made in the market, there’s usually a matching increase in the size calls for of both computer memory plus hard drive room. This is because the software program operated by means of these processor chips will inevitably rise in power to use the new know-how.
I抣l right away grab your rss feed as I can’t find your email subscription link or e-newsletter service. Do you’ve any? Kindly let me know so that I could subscribe. Thanks.
where to buy nolvadex pct When you are on post cycle therapy after Turinabol solo cycle it means that you re off the steroid cycle but your testosterone level is still reduced.
The mean total apparent plasma clearance of a single oral dose of 50 mg doxepin in healthy individuals is 0. doxycycline alcohol interaction Vitamin C deficiency.
By virtue of its MAO B selectivity at the FDA approved dosing, selegiline requires much less dietary vigilance and is also theoretically safer for use with medically complex patients torsemide to lasix
In this test, all groups of epileptic rats exhibited an enhanced aversive or aggressive response to pick up, but the difference to controls became statistically different only for the SE plus saline group 1, the SE plus phenobarbital group 2, and the SE plus phenobarbital plus high bumetanide group 4 groups, respectively Fig buy lasix online australia
priligy tablets price P P P B or Dunnett D multiple comparisons tests; n 3
In initial experiments, we used an OPC specific inducible Sox2 knock out line Pdgfra creER T2 Sox2 fl fl buying cheap cialis online
prescription without a doctor’s prescription
Find side effects early cialis and viagra sales
Great read! The article is well-researched and insightful. Consider incorporating more visuals in your future articles for a more visually appealing presentation.
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.
Explore a curated selection of high-quality backlink sources that adhere to current SEO best practices.
Thanks for sharing. I read many of your blog posts, cool, your blog is very good.
The preliminary results cialis online ordering Pubmed also has enough fun from him
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article. https://www.binance.com/hu/register?ref=FIHEGIZ8
Your article helped me a lot, is there any more related content? Thanks!