25

I'm building a Node.js app with Connect/Express.js and I want to intercept the res.render(view, option) function to run some code before forwarding it on to the original render function.

app.get('/someUrl', function(req, res) {
    
    res.render = function(view, options, callback) {
        view = 'testViews/' + view;
        res.prototype.render(view, options, callback);
    };

    res.render('index', { title: 'Hello world' });
});

It looks like a contrived example, but it does fit in an overall framework I'm building.

My knowledge of OOP and Prototypal inheritance on JavaScript is a bit weak. How would I do something like this?

After some experimentation I came up with the following:

app.get('/someUrl', function(req, res) {

    var response = {};
    
    response.prototype = res;

    response.render = function(view, opts, fn, parent, sub){
        view = 'testViews/' + view;
        this.prototype.render(view, opts, fn, parent, sub);
    };

    response.render('index', { title: 'Hello world' });
});

It seems to work. Not sure if it's the best solution as I'm creating a new response wrapper object for each request, would that be a problem?

Chenmunka
  • 685
  • 4
  • 21
  • 25
Sunday Ironfoot
  • 12,840
  • 15
  • 75
  • 91
  • 1
    That is normally how you do it. If you put that in a middleware that is used in before the router you can set it up in a single place. – Ryan Olds Feb 14 '12 at 23:50
  • I thought of using middleware. Not sure if I want to override the response behaviour for the entire application, only for requests that are routed through my framework. – Sunday Ironfoot Feb 17 '12 at 09:13

4 Answers4

29

Old question, but found myself asking the same thing. How to intercept res render? Using express 4.0x something now.

You can use/write middleware. The concept was a bit daunting to me at first, but after some reading it made a little more sense. And just for some context for anyone else reading this, the motivation for overriding res.render was to provide global view variables. I want session to be available in all my templates without me having to type it in every res object.

The basic middleware format is.

app.use( function( req, res, next ) {
    //....
    next();
} );

The next param and function call are crucial to execution. next is the callback function, to allow multiple middleware to do their thing without blocking. For a better explanation read here

This can then be used to override render logic

app.use( function( req, res, next ) {
    // grab reference of render
    var _render = res.render;
    // override logic
    res.render = function( view, options, fn ) {
        // do some custom logic
        _.extend( options, {session: true} );
        // continue with original render
        _render.call( this, view, options, fn );
    }
    next();
} );

I've tested this code, using express 3.0.6. It should work with 4.x without issue. You can also override specific URLs combos with

app.use( '/myspcificurl', function( req, res, next ) {...} );
Lex
  • 4,749
  • 3
  • 45
  • 66
  • 1
    Does this override the function on every request, putting pressure on the GC? Would it make more sense to override the prototype? – jocull Jun 21 '16 at 19:15
  • This overrides on every request. It will override the prototype via property shadowing. Can override the prototype if you want to, but generally you want properties on the main object not deep in the prototype chain. The property look up time is faster this way. – Lex Jun 24 '16 at 09:47
  • 1
    I didn't get this answer at first, but after fiddling around myself a lot, now I do get it. You can checkout a variation of Lex his code on https://github.com/mettamage/renderExtMiddleware -- I don't use `_.extend(args)`. My code renders a normal view *except* when the client is not a browser, then it returns json. – Melvin Roest Nov 30 '16 at 20:11
  • 1
    Yeah could describe it better. Your example is using the default render function, I didn't know it would return json when different headers. The above example is defining a new function to handle rendering. And `_.extend` is totally optional, I was using the underscore library to extend an object. – Lex Nov 30 '16 at 22:17
  • +1 for mentioning underscore, that made the answer a bit more understandable -- I'm new(ish) to node.js. – Melvin Roest Dec 08 '16 at 23:37
16

It's not a good idea to use a middleware to override a response or request method for each instance of them because the middleware is get executed for each and every request and each time it get called you use cpu as well as memory because you are creating a new function.

As you may know javascript is a Prototype-based language and every object has a prototype, like response and request objects. By looking at code (express 4.13.4) you can find their prototypes:

req => express.request
res => express.response

So when you want to override a method for every single instance of a response it's much much better to override it in it's prototype as it's done once is available in every instance of response:

var app = (global.express = require('express'))();
var render = express.response.render;
express.response.render = function(view, options, callback) {
    // desired code
    /** here this refer to the current res instance and you can even access req for this res: **/
    console.log(this.req);
    render.apply(this, arguments);
};
Ali
  • 21,572
  • 15
  • 83
  • 95
  • 3
    I don't know why this comment is not the accepted one! it is the absolute right answer!. – Ahmed Ayoub Dec 01 '17 at 21:43
  • overriding functions this way is possible but considered code pollution. regardless if the logic is called from a middleware or by overriding the render function they are almost identical performance-wise, 1 extra function call in the middleware should never be taken into consideration when evaluation implementations for a web server. there are way more bottlenecks on the network layer than an extra function call. though this has worst performance than using middleware due to the render variable and the `.apply` function call at the end... – Bamieh Feb 26 '23 at 16:06
8

The response object doesn't have a prototype. This should work (taking Ryan's idea of putting it in a middleware):

var wrapRender = function(req, res, next) {
  var _render = res.render;
  res.render = function(view, options, callback) {
    _render.call(res, "testViews/" + view, options, callback);
  };
};

However, it might be better to hack the ServerResponse.prototype:

var express = require("express")
  , http = require("http")
  , response = http.ServerResponse.prototype
  , _render = response.render;

response.render = function(view, options, callback) {
  _render.call(this, "testViews/" + view, options, callback);
};
Linus Thiel
  • 38,647
  • 9
  • 109
  • 104
  • Thanks for that. Although this seems to break how it looks for the layout/master view as well, it tries to look for 'testViews/layout.ejs'. I did look at the source and saw that a _render method is already define on the response object, could this cause problems? – Sunday Ironfoot Feb 17 '12 at 09:12
  • Yes, that's correct - it will prepend "testViews/" to all views. It has nothing to do with `ServerResponse._render`. What are you trying to accomplish? – Linus Thiel Feb 17 '12 at 12:52
  • Tried to implement the ServerRespone.prototype hack and failed. Guess this worked in node 0.6/express 2? Do you have any idea how to make this work in node 0.8 and express 3? – Andreas Hultgren Feb 09 '13 at 22:23
4

I recently found myself needing to do the same thing, to provide a configuration-specific Google Analytics property id and cookie domain to each of my templates.

There are a number of great solutions here.

I chose to go with something very close to the solution proposed by Lex, but ran into problems where calls to res.render() did not already include existing options. For instance, the following code was causing an exception in the call to extend(), because options was undefined:

return res.render('admin/refreshes');

I added the following, which accounts for the various combinations of arguments that are possible, including the callback. A similar approach can be used with the solutions proposed by others.

app.use(function(req, res, next) {
  var _render = res.render;
  res.render = function(view, options, callback) {
    if (typeof options === 'function') {
      callback = options;
      options = {};
    } else if (!options) {
      options = {};
    }
    extend(options, {
      gaPropertyID: config.googleAnalytics.propertyID,
      gaCookieDomain: config.googleAnalytics.cookieDomain
    });
    _render.call(this, view, options, callback);
  }
  next();
});

edit: Turns out that while this all might be handy when I need to actually run some code, there's a tremendously simpler way to accomplish what I was trying to do. I looked again at the source and docs for Express, and it turns out that the app.locals are used to render every template. So in my case, I ultimately replaced all of the middleware code above with the following assignments:

app.locals.gaPropertyID = config.googleAnalytics.propertyID;
app.locals.gaCookieDomain = config.googleAnalytics.cookieDomain;
Timothy Johns
  • 1,075
  • 7
  • 17
  • Two really good points. The locals property would fit most cases. And if no options are passed to render the callback becomes undefined, and all hell breaks loose. – Lex Dec 07 '15 at 10:26