Reimplementing Express: Part 2
A couple of weeks ago I decided to begin reimplementing Express as a side project. In part 1 I talked about how to initialise a new Hexpress app, and how to add some custom methods to the response object to make responding easier.
In this post, I’ll talk about how to begin implementing
If you’re familiar with Express you’ll know that once you’ve got a new Express app you can easily associate handler functions with different methods and endpoints. A simple example:
You can see that Express even allows you to describe parameterised routes (:id) where id is like a placeholder in the URL for any given id (e.g. /api/hexes/4 or /api/hexes/13). You then have access to the parameters on the
req.params object and can use the actual ID provided to do whatever you need to do. (We’ll worry about parameterised routes in a future post).
This simple mapping of endpoints described as strings to handler functions makes Express really enjoyable and straightforward to use. But how can we implement that sort of functionality for ourselves?
Well, first we need to give our app a
get method that accepts a string and a handler function. And the most obvious way to store which functions should handle which requests could be to store the mappings in an object:
Our rotueMappings object would, at this point, look like this:
'GET /api/hexes': (req, res) => res.send('Loads of hexes'),
'GET /api/cauldrons': (req, res) => res.send('Loads of cauldrons')
Receiving a request
What happens when our app receives a request? Previously we set up our Hexpress server to respond with a default response to all requests, but now we need to take another look at what happens when a request comes in and figure out which of our various handlers that we’ve collected is the one that ought to respond.
We can figure out what method was used (GET, PUT, POST, DELETE etc) and what the URL was (/api/witches, api/spells, /api/cauldrons etc) from the request object, and with that information we can find the key of the
routeMappings object and therefore locate the correct handler!
If we don’t have anything set up to deal with the specific endpoint, we’ll just send back a default response that says ‘Cannot GET /api/wizards’ for example, and a 404 status code.
And this happens quickly, since objects allow for immediate lookup. A sensible choice for mapping strings to functions, right?
That’s not how Express actually works.
Take a look at this code:
We’ve set up a handler for the /api endpoint on line 4, and then set up another handler for the same endpoint on line 8.
If you made a GET request to localhost:3000/api what would you expect the response to be? “First function”, or “Second function”? In other words, would you expect Express to overwrite the first handler with the second handler? That’s what you might expect, given that we know objects cannot have duplicate keys. When we try and add a new handler to the same path, it probably just overwrites the hander that was added first.
Try it out if you want. The response is “First function”. So, uh, the above hypothesis was wrong. The second function does not overwrite the first one.
Okay, that sort of makes sense. If you try and add a handler to an endpoint that already has a handler, maybe Express just chooses to ignore it.
But take a look at this.
If you made a request to localhost:3000/api now you would see that roughly half the time the response is “First function” and half the time it is “Second function”. That function next() that I call in the first handler function tells Express to go on and try the ‘next middleware function’. So both handlers are being stored somewhere, and Express tries the first one that we set up first, and if it’s asked to move onto the next one, then it will move on and try the second handler function.
Which means that it’s time to move away from storing string-handler mappings in an object, where we are limited by the unique keynames and unordered entries, and use an array instead. As we will see later, using an array will have lots of other benefits.
Storing handlers in an array
Okay, let’s stop calling the handler functions handlers now and call them middlewares, which is the word Express uses. A middleware is, as its name suggests, a function or a process ‘in the middle’. In this case, we’re talking about a number of functions that the request/response objects will be passed through in order until the response is sent back.
To begin with, let’s store the middlewares in an array which preserves the order in which they were added and makes sure every function is still available, even if there’s more than one function added to the same route (see functions 1 and 3 in the above diagram). We’ll have to store each middleware as as object with information about the function itself and what route it is supposed to match, for example:
handler: (req, res) => res.send('All good')
Now when our app receives a request, instead of as immediately accessing the corresponding handler from a object, we will need to loop through the array of middlewares and select the first one where the method and the path match those of the request.
The things to notice in the above code are:
- We set up an empty array of middlewares on the new Hexpress app when it is created (line 2)
- When someone calls app.get() we push a new object into the array of middlewares. The object stores information about the path, method and the middleware function itself (line 23)
- When the app receives a request (line 6) we loop through the middlewares array until we find the one that matches the request. Then we return (line 17) which breaks us out of the loop so that we don’t check any more middlewares.
We are getting closer to mimicking Express’s functionality because we’re now storing all middlewares in an array, but we still can’t do that thing we talked about earlier where we could have two (or more!) handlers for the same route and choose to skip over one if we wanted. Here’s a demo of that for a reminder:
With the code we’ve got so far, there’s no way we could expect calling next() to skip over that first middleware and move us onto the next matching one in the array of middlewares.
If we tried calling next in our handler, we’d probably get an error saying that
undefined is not a function because when we call the handler, we’re not passing in a third argument for it to use:
In the next part of this series, I’ll talk about how to modify our
listen method so that is passes in a third parameter to any middleware function it calls. We will refactor to use recursion, passing the request and response objects through numerous middleware functions in the array until finally, a response is sent back to the client.
You can see the full source code that I’ve completed so far here.
Hope you enjoyed this post! If you did, give it a clap and maybe follow me?
See you soon!