expressjs / expressjs.com

Home Page:https://expressjs.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Suggestion - standardise middleware locals context

MrBr opened this issue · comments

In the documentation for writing the middleware there is an example:

const express = require('express')
const app = express()

const requestTime = function (req, res, next) {
  req.requestTime = Date.now()
  next()
}

I'd like to point out that this seems like a bad practice and would like to suggest a change.

Adding new attributes to an uncontrolled object could lead to name conflicts. This scenario isn't very likely to happen but leaves room for an error.

The solution is rather simple, if it makes sense to add values to req object there should be a standardised API or in this case attribute for doing so just like with res object.

From design perspective it makes sense that there is a scoped place (object) that can be edited and modified safely. This attribute(s) represent a contract that both provider and consumer should then respect.

Currently, there is no clear difference between writing values in req or res objects and I believe it makes sense to define cases clearly.

Propositions:

  1. Define res.locals as a main way for passing the values
  2. Add req.locas as a specified attribute for passing request related values

Conceptually, there may be a difference between req and res values but it's a bit vague. For example, requestTime value is somewhat related to the request, but the problem comes with data passed to add more information to the context for accessing the actual response data. Where should it be stored?

Another reason for having req and res values separated might be to prevent renderer from accessing all the data in the context, but I am uncertain if it's a valid concern.

If res.locals is so tightly coupled with renderer then the name is ambiguous.

All this, in my opinion, suggest that it'd be better to have one scope for storing the contextual values. By slightly loosening up res.locals definition, and making it less coupled with the renderer (conceptually) it could be a good place for storing all the data.

Hi @MrBr thank you for the suggestion! This has been brought up several times, and it seems there is confusion over res.locals and we tried to clear that up with our documentation.

  1. As noted in https://expressjs.com/en/4x/api.html#res.locals the res.locals is just for exposing specific information to your rendered templates. That is the purpose of this object, not a general store of anything you want (unless you do infact what to send all that information to your rendered templates, of course).
  2. It is perfectly acceptable to store anything you want on our req and res objects, particularly when you are authoring middleware. This is why we should example of doing that. This is how all our middleware work in the ecosystem, and why you can have things like req.session (session middleware are setting a setting property directly on req), res.body and req.files (body parsers directly set a property on req) and many others.

There is nothing wrong with a middleware decorating req and res with additional properties, and in fact is encouraged as our documentation suggests.

Thank you for this fast clarification I understand intent better.

However, I still feel like it'd be a better design decision to have a scoped place for storing contextual values that is not req, res or res.locals object.

In my opinion this isn't a good example of decoration. When decorating an object the initial shape should be preserved (wrapped). In the examples above it's not new behaviour that is being added, just data without any boundaries.

We can look that addition as a behaviour of adding data to the context and I believe it makes sense to encapsulate that behaviour, put some boundaries around it.

A different thing would be if I'd like to change the logic of send method. Then this would indeed be a behavioural change but it still make sense to preserve the original.

I understand this is more theoretical thing than practical. It's still an interesting topic.

Hi @MrBr thank you for that. I'm not really sure what you are envisioning there, and what you mean, exactly. Perhaps it would help if you could build out perhaps some kind of prototype of what you are envisioning? For example, fork expressjs/express repo and build out the changes to Express.js for providing the encapsulation you are thinking and can link that fork here to help us better understand the conversation?

I assume from your text it is more than just Express.js adding a req.data = {} bag where middleware can just pile on properties, because that is no better/less conflicts/any better boundaries than just req or res itself -- any given middleware still won't know if their chosen property will conflict or not better than if it were just on req.

Just adding an attribute would suffice. I'll elaborate in details.

Every features has a definition, I'm arguing that adding value to a local context is a feature. In express there isn't such a definition in the API specification. This functionality is only briefly mentioned in an example about writing middleware.

I believe this is the first problem and the reason why there is still an ongoing debate in the community on this topic. Adding a page similar to existing Overriding the Express API, something like Passing value to middleware would be very helpful.

Currently, there is no local context, instead it's advised to add values to req or res objects. I've purposefully emphasised a feature, because if it's looked as a thing for itself, local context would be something tangible, something with boundaries. Just like locals for renderer.

What we have now is a convention. The req and res object represent a local context where temp values can be saved. This is where we reach to the boundaries problem. Those objects are used for providing the Express API and in the same time used for storing the values. Those should be 2 separate things, or if we get back to my first point, adding value to the context should be API functionality.

Because those objects are "abused" this can lead to situation where by saving a value to the local context API can be overridden. So, when I say boundaries I mean boundaries between local context and API object. Local context should be a part of the API. Then, boundaries would practically assure that no new Express API feature will override a value (or vice versa), and it should never be possible for such a thing to happen.

As you mentioned, middleware can still override the values if they use the same key but there is a big difference. Those middleware are composed by someone else so there will be no conflict with the library itself. This is the boundary border, you only guarantee for API not how it's used.

Reserved field

Getting back to the reserved field. That would be enough, in a theory an interface such as one of localStorage could be created but it's just an overhead.

An idea!

I've mentioned local context so many times that I figured that context is a good name for this feature. And while writing this I've come up to an idea. What if the middleware signature would be changed to:

const middleware = ({ req, res, context }, next) => {
  context.session = '';
};

The value doesn't belong to request or response, it belongs to the current middleware context. What do you think?

P.S.

Why is aforementioned decoration bad.

const obj = {
  override: 1
};
const middleware = (ref, next) => {
  obj.override = null;
  next();
}

middleware(obj, someNext);
// The middleware spills out of the scope and we should avoid such a behaviour because there is no guarantee how obj will be used later
// I understand this is currently not causing any issues but conceptually I believe it's not good pattern

// The right way to do a decoration would be to leave the object intact
// For example, with a bit different next signature
const obj = {
  override: 1
};
const middleware = (ref, next) => {
  next({ ...ref, override: null });
}

// This approach allows something extra
// Because the initial object hasn't been changed, we can still depend on it
const obj = {
  cb: () => {}
};
const middleware = (ref, next) => {
  const newCb = () => {
   log();
   ref.cb(); // This wouldn't be possible in the first example
  }
  next({ ...ref, cb: newCb });
}