1. 9
  1.  

  2. 3

    If your middlewares represent simple, independent operations, I still think middleware is a poor way of expressing these operations, but it is mostly benign. The trouble begins when the operations become complex and interdependent.

    I would disagree, and I think what you are struggling with is language/framework choices not the middleware concept. Just to expand on your example with 100% admin request; over time your main dispatch middleware will be so complicated nobody will understand what is going on. I’ve seen complicated projects with these “grand central dispatchers” bloating up to the point of unmanageable code. I would still keep an auth middleware that reads the user on top and adds it as a “context” to request (however express.js might do it), and let the chain continue. That one dead straight list of middleware will save you nightmares of testing and keep your code simple. Your central dispatch middleware is a bad smell to me.

    1. 1

      I’ve seen non-trivial middleware stacks in Ruby, PHP, and Node.js and would apply my analysis to all of them. I don’t think it’s language specific.

      “Context” is a better name than “request” for an arbitrary grab bag of properties, but req.context.isAdmin = … or req.context.account = ... isn’t really any morally different than req.isAdmin = ... or req.account = ....

      If your “grand central dispatcher” is complicated, that means that the operations you are performing on your requests are complicated. Breaking your dispatcher up into middleware won’t make the complexity go away – it will just make the complexity implicit in the assumptions that each middleware makes on the structure and meaning of the universal “context” object, rather than having it be expressed explicitly through the parameters and return types of functions, and the control flow of your dispatcher.

      But I don’t necessarily advocate one big “grand central dispatcher”. You can break it up. But if you break it up, I just advocate against decomposing it into multiple middleware. Instead, decompose it into functions with meaningful return values, whose parameters reflect their actual dependencies, where the control flow and interdependencies between these functions are explicit, instead of into crippled “middleware” functions that are not allowed to have a meaningful return value and can only communicate via implicit interactions inside an ill-typed, arbitrary grab bag of properties.

      Such functions, I would argue, are easier to test than middlewares. In order to test a middleware, you must artificially construct an http request and response, when likely the operation your middleware performs only cares about some parts of the request, and effects some parts of the request (or response).

      In order to test e.g.

      const rateLimitingMiddleware = async (req, res) => {
        const ip = req.headers['ip']
        db.incrementNRequests(ip)
        if (await db.nRequestsSince(Date.now() - 60000, ip) > 100) {
          return res.send(423)
        }
      }
      

      You have to

      const req = {headers: {ip: '1.1.1.1'}}
      db.incrementNRequests = sinon.stub()
      db.nRequestsSince = sinon.stub.returns(101)
      const res = { send : sinon.stub() }
      await rateLimitingMiddleware(req, res)
      sinon.assert.calledOnce(db.incrementNRequests)
      sinon.assert.calledWith(res.send, 423)
      

      whereas for

      const shouldRateLimit = async (ip) => {
        db.incrementNRequests()
        return await db.nRequestsSince(Date.now() - 60000, ip) < 100
      }
      

      Has one less mock, at least, and doesn’t require you to construct these nested request and response data structures.

      db.incrementNRequests = sinon.stub()
      db.nRequestsSince = sinon.stub.returns(101)
      const result = await shouldRateLimit('1.1.1.1')
      sinon.assert.calledOnce(db.incrementNRequests)
      expect(result).toEqual(true)
      
    2. 3

      I enjoyed the article and I think it’s well-written, but I don’t agree with the analogy made, and therefore neither do I agree with the proposed solution to what I don’t agree is an actual problem to be solved.

      I don’t buy the analogy made to compiler stages, even though it was described nicely. I don’t agree that middlewares should not be composed together. I also believe that the problem of multiple database queries for the same data is orthogonal to the topic — what you want here is per-request caching, and not a design compromise.

      1. 1

        I think you’re onto something here but I’m not sure what. It does get at the deep, visceral dislike I have of most web frameworks: pervasive mutability, their tendency to clog your app with all sorts of web-only goop, and APIs the that are too big. Specifically, the application of MVC to web frameworks feels like it “works” mostly because MVC separates concerns just enough to not make huge messes; and it isn’t too hard to learn. It doesn’t seem like it is the best fit to the problem, however, for the reasons the author states.

        The problem is translating the web junk into actions on a domain model with a minimum of boilerplate. There has to be a better way than model/controller.