Jeach
Jeach

Reputation: 9042

Can't add an express route unless passed in as parameters

I'm trying to create Node modules which map it's own routes. I've provided a simplified example below (removed all other functions mapped in the object returned and any code to simplify the example).

My initial attempt was to do it like something like this:

File: web-core.js

function initRoutes(app) {
  var express = require('express');  
  var router = express.Router();

  router.get("/basic", function(request, response) {
    response.json({ success: true, path: '/auth/basic' });
  });

  router.get("/oauth2", function(request, response) {
    response.json({ success: false, path: '/auth/oauth2' });
  });

  router.get("/openid", function(request, response) {
    response.json({ success: false, path: '/auth/openid' });
  });

  app.use('/auth', router); // mount the module
}

function init(app) {
  initRoutes(app);                     //-- This does work (but don't like it)
  //initRoutes(require('express')());  //-- This does NOT work!
}

module.exports = (function() {  
  return {    
    init : init
  };  
}());

And from my Node entry point:

var express = require('express');
var app = express();

app.use(express.static('public'));

var webAuth = require("./web-auth.js");

webAuth.init(app);  //-- Wish I didn't have to call this!

var listener = app.listen(process.env.PORT, function () {
  console.log('Your app is listening on port ' + listener.address().port);
});

Now, my primary objective was to NOT have to call webAuth.init(app) (and indirectly use initRoutes(require('express')()); instead). I would like to have the module not rely on explicit or external initialization.

But for some reason, when I use initRoutes(require('express')()); it just doesn't work. It seems to only work when I call webAuth.init(app) and pass in the app object.

I'm not sure what I'm missing here. I was told that Node will cache all instances returned by require('express'), so technically both ways should be the same and work the same.

All the express.Router() examples I've seen on the web provide simple use-case and not "modularized" like I'm trying to do.

Does anyone know why it's not working?


Update: 2018.02.04 at 18:27

I've created a Glitch project with sample code in order for anyone to understand, which can be found here.

It seems there is always some issue or another. Either lack of documentation or some mysterious 'thing' not working.

So as advised by the answers provided, I decided to initialize externally, such as:

var webAuth = require("./web-auth.js");
webAuth.init(app, router);

And in my module, I now have:

function init(app, router) {
  console.log("### Initializing '" + MODULE_NAME + "' module for '" + BASE_PATH + "'");

  initFilters(router);
  initRoutes(app, router);
}

This works well. When I call GET /auth/basic, both my filter and /auth/* routes work as expected.

But NOW, I started creating my second module which is domain specific, so we'll say that the API should get mapped to /domain/*. So obviously, this second module should now use a new instance of express() and express.Router(), no? Otherwise it wouldn't make no sense to reuse the same ones.

So when I call this:

app = express();
router = express.Router();

var webDOMAIN= require("./web-domain.js");
webDOMAIN.init(app, router);

You'd expect that module to work also on /domain/* paths ... but it doesn't!!! Damn, it's frustrating working with NodeJS and ExpressJS. Seems my experience with these two technologies are proving more frustrating by the day than any other in my 30+ years experience.


Update: 2018.02.06 at 10:59

Ok, I've finally figured it out!!! Instead of posting it all here, I will write an extensive answer to my own question.

Upvotes: 2

Views: 680

Answers (3)

Jeach
Jeach

Reputation: 9042

Ok, so there are essentially a few important things to understand with ExpressJS (which I didn't quite grasp initially).

The following call is cached through NodeJS's require() mechanism:

var express = require('express');

Which essentially means that if you call the above many times or from other modules, you essentially get the 'same' function back.

Next is where we get an ExpressJS application context. It is important to understand that for 'each' call, you get a NEW context.

var app = express();

This is where I was going wrong; (a) not realizing it was a different context, and (b) not reusing the primary context when adding my routes from express.Router() instances. Initially I was trying to create a new context for each module, which is NOT required. I'll explain it some more later.

And lastly, each following call will create a new 'router' instance where you can map your routes.

var router = express.Router();

You WILL want a new route for each module you create. And as indicated above, you will add each module route to your 'main' application context, as follows:

app.use(BASE_PATH, router); 

So you essentially 'share' a single instance of app, but create a new router instance for each module. Since I was creating both an 'app' context and adding the 'router' to it, only the last one was working properly.

Here is my NodeJS + ExpressJS code with a few sample modules:

File server.js:

var express = require('express');
var app = express();
var router = express.Router();

app.use(express.static('public'));

//------------------------------------------------------------
// Modules here...
//------------------------------------------------------------

var webAuth = require("./web-auth.js");
webAuth.init(app);

var webXYZ = require("./web-xyz.js");
webXYZ.init(app);

var webABC = require("./web-abc.js");
webABC.init(app);

//------------------------------------------------------------

// listen for requests :)
var listener = app.listen(process.env.PORT, function () {
  console.log('Your app is listening on port ' + listener.address().port);
});

File web-abc.js:

// ----------------------------------------------------------------------------
// Private Properties
// ----------------------------------------------------------------------------

var one = 1;
var two = 2;
var foo = "foo";
var bar = "bar";

var MODULE_NAME = 'web-abc';
var BASE_PATH = '/abc';


// ----------------------------------------------------------------------------
// Private API's
// ----------------------------------------------------------------------------

/**
 * Route middleware that will happen on every request
 */
function initFilters(router) {
  console.log("### Initializing filter!!!");

  router.use(function(req, res, next) {
      // log each request to the console
      console.log("Filter 1: " + req.method, BASE_PATH + req.url);

      // continue doing what we were doing and go to the route
      next(); 
  });

  router.use(function(req, res, next) {
      // log each request to the console
      console.log("Filter 2: " + req.method, BASE_PATH + req.url);

      // continue doing what we were doing and go to the route
      next(); 
  });
}

function initRoutes(app, router) {
  console.log("### Initializing routes!!!");

  router.get("/foo", function(request, response) {
    console.log("Endpoint: " + BASE_PATH + "/foo");
    response.json({ success: true, path: BASE_PATH + '/foo' });
  });

  router.get("/bar", function(request, response) {
    console.log("Endpoint: " + BASE_PATH + "/bar");
    response.json({ success: false, path: BASE_PATH + '/bar' });
  });

  app.use(BASE_PATH, router); // mount the module
}

function getFoo() {
  return foo;
}

function setFoo(value) {
  foo = value;
}

function getBar() {
  return bar;
}

function setBar(value) {
  bar = value;
}

function init(app) {
  console.log("### -------------------------------------------------------------");
  console.log("### Initializing '" + MODULE_NAME + "' module, for path '" + BASE_PATH + "'");

  var express = require('express');
  var router = express.Router();

  initFilters(router);
  initRoutes(app, router);
}

// ----------------------------------------------------------------------------
// Module Export
// ----------------------------------------------------------------------------

module.exports = (function() {

  return {

    // ------------------------------------------------------------------------
    // Public Properties
    // ------------------------------------------------------------------------

    pub1: 8,
    pub2: 9,

    // ------------------------------------------------------------------------
    // Public API's
    // ------------------------------------------------------------------------

    getFoo  : getFoo,
    setFoo  : setFoo,

    getBar  : getBar,
    setBar  : setBar,

    init    : init
  };

}());

File web-xyz.js:

// ----------------------------------------------------------------------------
// Private Properties
// ----------------------------------------------------------------------------

var one = 1;
var two = 2;
var foo = "foo";
var bar = "bar";

var MODULE_NAME = 'web-xyz';
var BASE_PATH = '/xyz';


// ----------------------------------------------------------------------------
// Private API's
// ----------------------------------------------------------------------------

/**
 * Route middleware that will happen on every request
 */
function initFilters(router) {
  console.log("### Initializing filter!!!");

  router.use(function(req, res, next) {
      // log each request to the console
      console.log("Filter 1: " + req.method, BASE_PATH + req.url);

      // continue doing what we were doing and go to the route
      next(); 
  });

  router.use(function(req, res, next) {
      // log each request to the console
      console.log("Filter 2: " + req.method, BASE_PATH + req.url);

      // continue doing what we were doing and go to the route
      next(); 
  });
}

function initRoutes(app, router) {
  console.log("### Initializing routes!!!");

  router.get("/foo", function(request, response) {
    console.log("Endpoint: " + BASE_PATH + "/foo");
    response.json({ success: true, path: BASE_PATH + '/foo' });
  });

  router.get("/bar", function(request, response) {
    console.log("Endpoint: " + BASE_PATH + "/bar");
    response.json({ success: false, path: BASE_PATH + '/bar' });
  });

  app.use(BASE_PATH, router); // mount the module
}

function getFoo() {
  return foo;
}

function setFoo(value) {
  foo = value;
}

function getBar() {
  return bar;
}

function setBar(value) {
  bar = value;
}

function init(app) {
  console.log("### -------------------------------------------------------------");
  console.log("### Initializing '" + MODULE_NAME + "' module, for path '" + BASE_PATH + "'");

  var express = require('express');
  var router = express.Router();

  initFilters(router);
  initRoutes(app, router);
}

// ----------------------------------------------------------------------------
// Module Export
// ----------------------------------------------------------------------------

module.exports = (function() {

  return {

    // ------------------------------------------------------------------------
    // Public Properties
    // ------------------------------------------------------------------------

    pub1: 8,
    pub2: 9,

    // ------------------------------------------------------------------------
    // Public API's
    // ------------------------------------------------------------------------

    getFoo  : getFoo,
    setFoo  : setFoo,

    getBar  : getBar,
    setBar  : setBar,

    init    : init
  };

}());

File web-auth.js:

// ----------------------------------------------------------------------------
// Module Dependencies
// ----------------------------------------------------------------------------

//var mongoose = require('mongoose');
//var Schema = mongoose.Schema;

// ----------------------------------------------------------------------------
// Private Properties
// ----------------------------------------------------------------------------

var one = 1;
var two = 2;
var foo = "foo";
var bar = "bar";

var MODULE_NAME = 'web-auth';
var BASE_PATH = '/auth';


// ----------------------------------------------------------------------------
// Private API's
// ----------------------------------------------------------------------------

/**
 * Route middleware that will happen on every request
 */
function initFilters(router) {
  console.log("### Initializing filter!!!");

  router.use(function(req, res, next) {
      // log each request to the console
      console.log("Filter 1: " + req.method, BASE_PATH + req.url);

      // continue doing what we were doing and go to the route
      next(); 
  });

  router.use(function(req, res, next) {
      // log each request to the console
      console.log("Filter 2: " + req.method, BASE_PATH + req.url);

      // continue doing what we were doing and go to the route
      next(); 
  });
}

function initRoutes(app, router) {
  console.log("### Initializing routes!!!");

  router.get("/basic", function(request, response) {
    console.log("Endpoint: " + BASE_PATH + "/basic");
    response.json({ success: true, path: BASE_PATH + '/basic' });
  });

  router.get("/oauth2", function(request, response) {
    console.log("Endpoint: " + BASE_PATH + "/oauth2");
    response.json({ success: false, path: BASE_PATH + '/oauth2' });
  });

  router.get("/openid", function(request, response) {
    console.log("Endpoint: " + BASE_PATH + "/openid");
    response.json({ success: false, path: BASE_PATH + '/openid' });
  });

  app.use(BASE_PATH, router); // mount the module
}

function getPub1() {
  return this.pub1;
}

function getPub2() {
  return this.pub2;
}

function getPub3() {
  return this.pub3;
}

function getOne() {
  return one;
}

function getTwo() {
  return two;
}

function getFoo() {
  return foo;
}

function getBar() {
  return bar;
}

function setBar(value) {
  bar = value;
}

function init(app) {
  console.log("### -------------------------------------------------------------");
  console.log("### Initializing '" + MODULE_NAME + "' module, for path '" + BASE_PATH + "'");

  var express = require('express');
  var router = express.Router();

  initFilters(router);
  initRoutes(app, router);
}


// ----------------------------------------------------------------------------
// Model Definition
// ----------------------------------------------------------------------------

// var templateSchema = new Schema({
//   title : String,
//   author : String,
//   body : String,
//   comments : [{ body: String, date: Date }],
//   date : { type: Date, default: Date.now },
//   hidden : Boolean,
//   tags: { type: [String], index: true }, // field level index
//   meta : {
//     votes : Number,
//     favs :  Number
//   }
// });


// ----------------------------------------------------------------------------
// Module Export
// ----------------------------------------------------------------------------

module.exports = (function() {

  return {

    // ------------------------------------------------------------------------
    // Public Properties
    // ------------------------------------------------------------------------

    pub1: 8,
    pub2: 9,

    // ------------------------------------------------------------------------
    // Public API's
    // ------------------------------------------------------------------------

    getPub1 : getPub1,
    getPub2 : getPub2,
    getPub3 : getPub3,
    getOne  : getOne,
    getTwo  : getTwo,
    getFoo  : getFoo,

    getBar  : getBar,
    setBar  : setBar,

    init    : init
  };

}());

With the above example, you essentially start a NodeJS server where three distinct module export their own web API routes (endpoints) and filters.

Supplemental Information

The ExpressJS people decided to return a function on the module.exports variable. Whereby I chose to export an object which maps methods. This object also has public properties (seen externally) and some private properties which can't be accessed externally.

You can make the module self contained. In my case, the 'web-auth.js' module binds all the possible authentication endpoints (routes) and has the validation and domain logic to process them. It receives a SessionID or ClientID and SecretKey combo to authenticate users. It uses Mongoose to store the SessionID and manage them, etc.

Upvotes: 1

yue you
yue you

Reputation: 2264

The require('express')() will create a new instance and it is not equal to the previous created app. So you assumption for Node will cache all instances returned by require('express') is not correct. I did check like this and it always return false.

function init(app) {
  console.log(app === require('express')()) // Always return false
  initRoutes(app);                     //-- This does work (but don't like it)
  //initRoutes(require('express')());  //-- This does NOT work!
}

Upvotes: 1

rollstuhlfahrer
rollstuhlfahrer

Reputation: 4078

There may be more than one express app at the same time. So you need to find the right app to assign the routes to. Since the app cannot do guessing, you need to tell the function which one to use.

As @yue you mentions, a call to require("express")() creates a new express instance. This means, you must pass the correct instance at some point in time.

My suggestion is getting rid of that initialization function since it isn't actually a constructor. You could move the initialization part into the exported function, so that the code works just like the express initializer. This way, you also archive the modularization you want to have.

web-auth.js

function init(app) {
  initRoutes(app);
}

module.exports = function(app) {  
  return init(app);
};

Then use it in your code as following:

require("./web-auth.js")(app);

Upvotes: 2

Related Questions