So, it was around June or July 2024, and I was making the same boring API in Express. I was wondering how the fu*k these things even work under the hood. So I thought, why not create my own small library like Express to understand the ‘under the hood’?
So I started building my own library, diesel.js. First, I just added simple methods like .get, .post, etc. But there was a problem. The user gives a path and a handler like this: app.get("/user", userHandler). I had to store these somewhere in my library class so when the client makes an API request to the server, I can route the client request to the correct API handler. For that, I used a simple object {} and stored handlers like this: diesel.routes[method+path] = Handler.
For handling the actual request, I made a handler function where I had to parse the incoming request URL (like http://localhost:3000/user to /user). then I had to match this path to my route to find the correct handler, like const matchedHandler = diesel.routes[req.method+path]. then, after I get the matching handler, I call the handler with Bun.js’s default incomingRequest object.
After building the first version, I was able to understand how Express & Fastify works under the hood. But my curiosity didn’t stop here.
My framework had many missing features that needed to get implemented, like middleware support and advanced path route matching like how express or fastify does.
So I started to add middleware support first.
But, but, but what is a middleware anyway? Middleware is just a function which gets invoked before the main handler.
and there are mainly two kinds of middlewares , one is global and second one is route specific.
But what is global middleware? Well, global middleware is just a list of middlewares which needs to get invoked first even if the route specific handler matches or not. It’s meant to run anyway.
And route specific middleware means it should only be invoked when the request comes to a specific route, like app.useMiddleware("/user", function(){console.log("i'm a global middleware")}).
Now this middleware function will only be invoked when the request comes on /user.
For storing global middleware, I used a normal array[], and for storing route specific middleware, I used a Map (I could have used a plain object too). Then I updated my handleRequest function to check if diesel.globalMiddleware.length > 0 exists, loop over them, and invoke them. Then to check for route specific middleware, I did const routeMiddleware = diesel.routeMiddleware(path), and if the middleware exists, then again loop over them and invoke each one by one. Finally, I was able to add middleware support to my library.
But the biggest problem still was the routing system. The plain object for storing the user given path was only capable of handling static routes like /user/login. It can’t do something like dynamic path matching (/user/:id, where :id means this ID can be anything) or a wildcard route like /assets/* (where * means that if the incoming request starts with the path /assets, then ignore the rest). Our plain object was incapable of doing these things. For handling this, I needed a smart router, something like a Trie router which can handle dynamic and wildcard routes.
Here is the link to the Medium article where i explained: How I built my own Trie based HTTP router.
And here is the diesel‘s github link : https://github.com/pradeepbgs/diesel
Top comments (0)