28

I'm doing a project in Node.js using express. Here's my directory structure:

root
|-start.js
|-server.js
|-lib/
|    api/
|        user_getDetails.js
|        user_register.js

The lib/api/ directory has a number of JS files relating to the API. What I need to do is make a sort of hooking system, that whenever one of the API functions gets requested from the express HTTP server, it does whatever action is specified in the according API handler. It's probably confusing, but hopefully you get the idea.

  1. Larry sends request via POST to get user details.
  2. Server looks in lib/api to find the function associated with that request.
  3. Server carrys out action and sends back data to Larry.

Hopefully you can help me out. I was thinking it could be done using prototypes, not sure though.

Thanks!

Domenic
  • 110,262
  • 41
  • 219
  • 271
  • I'm not sure I understand. You simply do `var m = require('./lib/api/user_getDetails.js')` and use that module in your response. Am I missing something? – freakish Jun 06 '12 at 13:14
  • I want it to load dynamically. That is, if I add a new API function, I don't have to manually require it. But I'm not sure how to accomplish this. –  Jun 06 '12 at 13:22
  • @Fike Dynamic server-side scripting? This does not sound good. :/ You know, there is a reason why classical solutions are called *classical*. :) But if you know what you are doing, then loading files is just a matter of working with `require('fs')`. See the documentation: http://nodejs.org/api/fs.html – freakish Jun 06 '12 at 13:26
  • @FlorianMargaine Yeah, but I'm not sure how to inject the code in so that the server routes requests for it. –  Jun 06 '12 at 13:31

3 Answers3

35

If you know where your scripts are, i.e. you have an initial directory, for example DIR, then you can work with fs, for example:

server.js

var fs = require('fs');
var path_module = require('path');
var module_holder = {};

function LoadModules(path) {
    fs.lstat(path, function(err, stat) {
        if (stat.isDirectory()) {
            // we have a directory: do a tree walk
            fs.readdir(path, function(err, files) {
                var f, l = files.length;
                for (var i = 0; i < l; i++) {
                    f = path_module.join(path, files[i]);
                    LoadModules(f);
                }
            });
        } else {
            // we have a file: load it
            require(path)(module_holder);
        }
    });
}
var DIR = path_module.join(__dirname, 'lib', 'api');
LoadModules(DIR);

exports.module_holder = module_holder;
// the usual server stuff goes here

Now your scripts need to follow the following structure (because of the require(path)(module_holder) line), for example:

user_getDetails.js

function handler(req, res) {
    console.log('Entered my cool script!');
}

module.exports = function(module_holder) {
    // the key in this dictionary can be whatever you want
    // just make sure it won't override other modules
    module_holder['user_getDetails'] = handler;
};

and now, when handling a request, you do:

// request is supposed to fire user_getDetails script
module_holder['user_getDetails'](req, res);

This should load all your modules to module_holder variable. I didn't test it, but it should work (except for the error handling!!!). You may want to alter this function (for example make module_holder a tree, not a one level dictionary) but I think you'll grasp the idea.

This function should load once per server start (if you need to fire it more often, then you are probably dealing with dynamic server-side scripting and this is a baaaaaad idea, imho). The only thing you need now is to export module_holder object so that every view handler can use it.

freakish
  • 54,167
  • 9
  • 132
  • 169
  • 2
    If you are calling the function a single time on startup, there's no reason to use the async versions; just use `fs.lstatSync` and `fs.readdirSync`. That also stops you from swallowing errors since exceptions will be thrown, instead of errors passed to callbacks and then ignored. – Domenic Jun 06 '12 at 13:55
  • @Domenic True. Somehow I got used to async programming and no longer think synchronously, hehe. :) By the way, you removed my `try{ }catch{ }` block. Indeed, it is not needed here, since the script will continue to work even if the module throws an exception. But this is no longer true for synchronous versions! – freakish Jun 06 '12 at 14:03
  • Great answer, but I'm still confused on how to make the server use the modules... An ideal solution would be to somehow use a prototype-based system I can extend (if that makes sense). Whatever script loads should have something like `Hook.add("user_getDetails"); Hook.user_getDetails.action = function() { console.log("user_getDetails method invoked!") };` Something like that, although I have no idea how this would work :P –  Jun 06 '12 at 14:04
  • @Fike No problem. In your scripts define functions, for example `module.exports = function(modules) { modules["user_getDetails"] = 'test'; };` and in my loader instead of `modules[path]=m;` do `require(path)(modules);`. Now `modules` object holds exactly what you want. The drawback is that you need all your scripts to follow the same format. – freakish Jun 06 '12 at 14:08
  • What about the server then? This is so confusing for me :/ –  Jun 06 '12 at 14:11
  • I want to add a word of warning to all of this - I'm currently having to *fix* a project that does this sort of thing all over the place, because the module requires are all hidden from the build system (they used typescript), so when a separate module tries to import the autoloader, it doesn't always know to include the proper files. – theaceofthespade Mar 28 '18 at 21:20
7

app.js

var c_file = 'html.js';

var controller = require(c_file);
var method = 'index';

if(typeof controller[method] === 'function')
    controller[method]();

html.js

module.exports =
{
    index: function()
    {
        console.log('index method');
    },
    close: function()
    {
        console.log('close method');    
    }
};

dynamizing this code a little bit you can do magic things :D

ZiTAL
  • 3,466
  • 8
  • 35
  • 50
3

Here is an example of a REST API web service that dynamically loads the handler js file based on the url sent to the server:

server.js

var http = require("http");
var url = require("url");

function start(port, route) {
   function onRequest(request, response) {
       var pathname = url.parse(request.url).pathname;
       console.log("Server:OnRequest() Request for " + pathname + " received.");
       route(pathname, request, response);
   }

   http.createServer(onRequest).listen(port);
   console.log("Server:Start() Server has started.");
}

exports.start = start;

router.js

function route(pathname, req, res) {
    console.log("router:route() About to route a request for " + pathname);

    try {
        //dynamically load the js file base on the url path
        var handler = require("." + pathname);

        console.log("router:route() selected handler: " + handler);

        //make sure we got a correct instantiation of the module
        if (typeof handler["post"] === 'function') {
            //route to the right method in the module based on the HTTP action
            if(req.method.toLowerCase() == 'get') {
                handler["get"](req, res);
            } else if (req.method.toLowerCase() == 'post') {
                handler["post"](req, res);
            } else if (req.method.toLowerCase() == 'put') {
                handler["put"](req, res);
            } else if (req.method.toLowerCase() == 'delete') {
                handler["delete"](req, res);
            }

            console.log("router:route() routed successfully");
            return;
        } 
    } catch(err) {
        console.log("router:route() exception instantiating handler: " + err);
    }

    console.log("router:route() No request handler found for " + pathname);
    res.writeHead(404, {"Content-Type": "text/plain"});
    res.write("404 Not found");
    res.end();

}

exports.route = route;

index.js

var server = require("./server");
var router = require("./router");

server.start(8080, router.route);

handlers in my case are in a subfolder /TrainerCentral, so the mapping works like this:

localhost:8080/TrainerCentral/Recipe will map to js file /TrainerCentral/Recipe.js localhost:8080/TrainerCentral/Workout will map to js file /TrainerCentral/Workout.js

here is a example handler that can handle each of the 4 main HTTP actions for retrieving, inserting, updating, and deleting data.

/TrainerCentral/Workout.js

function respond(res, code, text) {
    res.writeHead(code, { "Content-Type": "text/plain" });
    res.write(text);
    res.end();
}

module.exports = {
   get: function(req, res) {
       console.log("Workout:get() starting");

       respond(res, 200, "{ 'id': '123945', 'name': 'Upright Rows', 'weight':'125lbs' }");
   },
   post: function(request, res) {
       console.log("Workout:post() starting");

       respond(res, 200, "inserted ok");
   },
   put: function(request, res) {
       console.log("Workout:put() starting");

       respond(res, 200, "updated ok");
   },
   delete: function(request, res) {
       console.log("Workout:delete() starting");

       respond(res, 200, "deleted ok");
   }
};

start the server from command line with "node index.js"

Have fun!

serkanozel
  • 2,947
  • 1
  • 23
  • 27
JJ_Coder4Hire
  • 4,706
  • 1
  • 37
  • 25
  • I'm not 100% sure since I've only kinda just glanced at the code but doesn't this code have some directory traversal vulnerabilities? `../../../` – Tony Jul 19 '18 at 17:20
  • just a sample for people looking to know how to do it. Certainly needs to be hardened. – JJ_Coder4Hire Aug 08 '18 at 20:11
  • 3
    Gotta be careful, some people literally copy and paste code from this site xD hopefully they'll read these comments and look into directory traversal vulnerabilities. -cough- talking to you, reader. yes, you. o_o – Tony Aug 10 '18 at 05:25