Closed Pana closed 8 years ago
Middleware accomplish two things:
Both are problematic. To elaborate on the first, consider a typical express application:
app.use(logger());
app.use(needsAuth());
app.get('/my-route', require('./my-route'));
Then, imagine you want to add a route /log
for which you want to turn off request logging and authentication. You end up with awkward code that introduces a global whitelist of exceptions.
app.use(function(req, res, next){
if ('/log' === req.url) return next();
logger(req, res, next);
});
Assuming that through experience and discipline you avoid this pitfall, you probably then rely on defining middleware for each route through "sub-apps".
// boot.js
app.use(require('./my-route');
// my-route.js
const bodyParser = require('body-parser');
module.exports = express().get('/', bodyParser, function(req, res, next){
// req.body
});
This pattern, however, still has significant problems:
req.body
comes out of nowhere. It's unclear to someone who's never seen the codebase before that the presence of bodyParser
is what made req.body
exist.app.error
).bodyParser
will hold up the request or not.await
+ async
is the solution to all of these problems.
First, the assignment operator brings clarity:
async function (req, res) {
const body = await json(req); // explicit
}
function (req, res, next) {
req.body // where did this come from? (╯°□°)╯︵ ┻━┻
}
Second, errors are handled like everywhere else:
async function (req, res) {
try {
await json(req);
} catch (e) {
// buffering failed
// size limit exceeded
// parsing failed
}
}
If you don't specify try/catch
and instead the error is caught by micro
, it goes with a suggested code (err.statusCode
), or 500
if no information can be derived from the Error
.
This flexibility in error handling is very difficult to achieve with middleware. In general, middleware skip over subsequent routes and go to the error handler (also not very obvious).
Third, the presence of the await
keyword will tell you if the request is actually being defered or not.
async function (req, res) {
await rateLimit(req); // this holds up the request
log(body); // this doesn't hold up the request
send(res, 200, 'woot!'); // no `res` monkey-patching
}
Finally, you might still favor global middleware to avoid incessant repetition. You could successfully argue that if every route in your app requires logging, this pattern falls into "pre-mature optimization".
Fortunately for us, simple functions solve this without sacrificing explicitness:
import decorate from './my-middleware';
export default decorate(async (req, res) => {
// the introduction of re-usable logic is explicit and
// the programmer knows to look at `my-middleware.js`
});
As you can see, micro
goes beyond "syntax sugar" or "making async code look sync". It's about code that's easier to understand, debug and collaborate on.
FWIW decorators are middleware. I totally agree though, excited to see that node core is maybe going with promises finally! Hopefully then npm modules can finally converge on the same goal, async/await should have been added 4 year ago haha – we could all be using vanilla Node with promises, no frameworks, no koa, no micro, no express, no babel, utopia <3.
I'd also argue though that HTTP is a bad abstraction to begin with, I kind of like Lambda's approach there, it makes more sense for the general case, but there's always going to be weird points where you have to integrate with HTTP. You're not finding the granularity annoying? I like Lambda for data processing but for HTTP I find so many distinct functions is kind of just a pain in the ass haha
Another approach that I think would be interesting, potentially even nicer is a more functional approach like Ruby's Rack and the now super popular Redux. The "middleware" just become opaque objects all the way down with promises wrapped at will for whatever behaviour, but that wouldn't have the normalizing effect that promises does/will.
+1 to being lean/explicit and just using decorators for middleware
I'd also argue though that HTTP is a bad abstraction to begin with
Couldn't agree more with this :+1:, HTTP always tends towards lots of adhoc plumbing. I think what makes things like Lambda, Kafka et al conceptually nicer is that a microservice is something that simply consumes one log and produces another (as opposed to just "small service"). Then you have a fundamental way to stack component parts together. If you consider decoupling the request-response (i.e. that you could receive a response without a request, or make a request with multiple responses, or make a request that does not have a response, etc) then HTTP (responding directly to a request) just becomes one specific instance of the stream processing model. Would love to see a version of micro based on this, or otherwise, for more realtime communication (push by default rather than pull)..
For those new to higher order functions, the decorate
function could look like this:
const decorate = fn => (req, res) => {
// do some stuff here
fn(req, res)
}
what about having React hooks
concept to solve this?
yes, algebraic effects ftw
Please provide an example where React Hooks are better than async-await
Explicit. No middleware. Modules declare all dependencies
What does it mean by no middleware? why you design it like this.