Things I learned While Developing Express-Kun (Backend Helpers for express app development)
Muhammad Hanif
Posted on February 21, 2020
Intro
Hi guys, So a while back I developed a library that aim to simplify backend development. I don't want to build a framework, I just want to build something like lodash but for backend-specific development. So I built express-kun. a library that providing a set of helper that use functional programming mindset. the concept is simple, for example if you want to create reusable middleware you just pass a router then it will returned back a midlewared router
. you can checkout the documentation for more example here https://github.com/hanipcode/express-kun
About this series
So I thought it will really simple to implement this library. But I never felt so wrong, might be because my lack of knowledge about javascript especially about it prototypal nature I thought I want to documented the process so I write this series. here we go to the first bump
Things I just know about express -> router.get is not calling a get in the router class directly but it use a route object (Rotuer different than Route).
Really, this hit me hard. Till know even though I spent hours reading express Router's source code I don't understand how it really implemented. My understanding of it barely scratch the surface.
I thought this library would be very simple. I thought I should just implement it by replacing the router.get
to another function like this (I will simplify it but you can read the source code in the repo I linked above)
withMiddleware(router, middleware) {
router.get = function(path, ...handlers) {
router.get(path, middleware, handlers)
}
// other method more or less the same..
return router;
}
but I get this error
var __spreadArrays = (this && this.__spreadArrays) || function () {
^
RangeError: Maximum call stack size exceeded
I was stupid actually, If I think about it the previous code will do an infinite recursion. so after hit by this error I check at express source code for Router (https://github.com/expressjs/express/blob/master/lib/router/index.js). checkout line 507 - 513.
methods.concat('all').forEach(function(method){
proto[method] = function(path){
var route = this.route(path)
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
methods is an npm package that list available Http method. and then it just loops over the methdo name. and
proto[method] = function(path){
var route = this.route(path)
route[method].apply(route, slice.call(arguments, 1));
return this;
};
this is the interesting path. so for an router object it create handler for every http method. but that handler is actually applying a method on a route object (remember that Router and Route is different). each router have a route object. (you can see a route source code here https://github.com/expressjs/express/blob/master/lib/router/route.js).
Here is when I feel more stupid. after knowing that I don't go to the obvious answer but instead make my second mistake:
Things I just know about express -> Router() does not return a plain object but instead a callable function but with property.
to test this you can write something like you will see that router can be called as a function that shorthand for router.use
const router = new Router();
router(yourMiddleware);
I know this because my second attempt was I try to replace the get method from .get etc like below
withMiddleware(router, middleware) {
const routeObject = {};
routeObject.get = function(path, ...handlers) {
router.get(path, middleware, handlers)
}
// other method more or less the same..
return {
...router,
...routeObject
};
}
actually this work for a while, really. but the problem is when you want to split multiple endpoint to multiple router for example if you have routes.js
like below
import userRouter from './modules/user/user.routes';
import postRouter from './modules/post/post.routes';
const apiRouter = Router();
apiRouter.use('/users', userRouter);
apiRouter.use('/posts', postRouter);
then in your post.routes
const router = Router();
const errorHandledRouter = withErrorHandler(router, errorHandlerMiddleware);
const protectedRouter = withAuthMiddleware(errorHandledRouter);
protectedRouter.get('/', postController.getAll);
protectedRouter.post('/', postController.create);
protectedRouter.get('/:postId', postController.getPost);
protectedRouter.get('/:postId/comments', postController.getComments);
protectedRouter.post('/:postId/comments', postController.comment);
// other route
export default protectedRouter;
it hit me with an error:
TypeError: Router.use() requires a middleware function but got a Object
at Function.use (/Users/hanif/Projects/express-kun-example/node_modules/express/lib/router/index.js:458:13)
yup. it error at apiRouter.use('/posts', postRouter)
. because postRouter is something that returned from withMiddleware and it's not really a router function. it was an object created by spread operator. that was the problem. actually there are workaround to create the post.routes like below
const router = Router();
const errorHandledRouter = withErrorHandler(router, errorHandlerMiddleware);
const protectedRouter = withAuthMiddleware(errorHandledRouter);
protectedRouter.get('/', postController.getAll);
protectedRouter.post('/', postController.create);
protectedRouter.get('/:postId', postController.getPost);
protectedRouter.get('/:postId/comments', postController.getComments);
protectedRouter.post('/:postId/comments', postController.comment);
// other route
export default router;
you still exporting the main router. but every router will still be registered because withMiddleware actually just registering a middleware inside that router. but that's very counterintuitive and I don't want my library to be confusing.
finally I came to the obvious solution:
export default function withMiddleware(
router: Router,
middlewares: SupportedMiddleware
): Router {
let connectedMiddleware: RequestHandler[];
if (isMiddlewareArray(middlewares)) {
connectedMiddleware = middlewares;
} else {
connectedMiddleware = [middlewares];
}
router.get = function(path: PathParams, ...handlers: RequestHandler[]) {
const route = this.route(path);
route.get.apply(route, [...connectedMiddleware, ...handlers]);
return this;
};
router.post = function(path: PathParams, ...handlers: RequestHandler[]) {
const route = this.route(path);
route.post.apply(route, [...connectedMiddleware, ...handlers]);
return this;
};
router.put = function(path: PathParams, ...handlers: RequestHandler[]) {
const route = this.route(path);
route.put.apply(route, [...connectedMiddleware, ...handlers]);
return this;
};
router.delete = function(path: PathParams, ...handlers: RequestHandler[]) {
const route = this.route(path);
route.delete.apply(route, [...connectedMiddleware, ...handlers]);
return this;
};
return router;
}
the code above was current code in the repo. here you notice that I was using a normal function instead of arrow function. so I can still get the 'this' value of the router. then I put the method inside the route object. actually this was just modifying this code from the express codebase
proto[method] = function(path){
var route = this.route(path)
route[method].apply(route, slice.call(arguments, 1));
return this;
};
Outro
alright that was my stupid bump on developing my express-kun
library. if you can learn a thing or two, that's great. and if you like the concept of the library, do try it. Thanks!
Posted on February 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 21, 2020