In a recent project, we built an application on top of a legacy application’s APIS. The legacy application was built with MeteorJS, and we chose a MERN stack to extend it. We decided to leverage the existing codebase due to time constraints and wide areas of functional reuse.

Meteor Methods

MeteorJS is a reactive Javascript framework that connects the server and client by calling methods through remote procedure calls (RPC). These methods are used to perform some backend tasks like applying some business logic on the data coming from the client or saving it in the database. If you are familiar with REST APIs or HTTP, you can think of them like POST requests to your server.

In a basic app, defining a Meteor Method is as simple as defining a function.

Meteor.methods({
   print: function (name) {
     console.log(name);
     return name;         
   }
})

Now, the above Method is callable from the client using Meteor.call.

Meteor.call(print, {name: 'John' }, (err, res) => { 
  if (err) 
    alert(err);  
  else 
    print(res);
});

Distributed Data Protocol(DDP)

Meteor 0.2.0 was released in 2012 with a major update that introduced DDP. DDP is Meteor’s built in pub/sub protocol. It resolved the various problems that Javascript developers face, such as

  1. Querying a server side database.

  2. Sending the results back to the client.

  3. Pushing the changes to the client whenever anything changes in the database.

Basically, the client subscribes to the data, so the server keeps the client informed about the content of data which changes over time. Under the hood, DDP uses web sockets, e.g. In live chat applications like messenger, Instagram, WhatsApp, etc., it automatically updates the message list in a chat window.

Our Approach

After an intensive discussion, we decided to build the new features on Node.js which would expose some REST endpoints, and which our React clients would also call. Due to the strict deadlines, we decided not to rewrite existing APIs on Node. Rather, we decided to use existing Meteor methods. It sounds great right?

Problem

But the catch in the above approach was, “How would our React clients call Meteor Methods(RMI) as REST endpoints?” To solve this problem, we tried finding a Meteor package to convert Meteor methods into standard Rest endpoints. We found some packages like json-routes or restivirus, but using these packages requires invasive code changes which we were reluctant to make because of time investment and regression impact.

Therefore, we chose Meteor Rest, which makes your Meteor app accessible over HTTP. It has a set of various packages like:-

  1. simple:rest - just add the package, and all of your Meteor methods and publications will become accessible over HTTP.

  2. simple:rest-accounts-password - add this package to enable password login over HTTP.

  3. simple:authenticate-user-by-token - authenticate the user via auth token

Unfortunately, we encountered some problems when we used simple:rest. This package generates JSON HTTP APIs for all of your Meteor app's publications and methods. It works with all of your existing security rules and authentication. This can be useful for many things like:-

  1. Exposing an API for other people to get data.

  2. Integrating with other frameworks and platforms without having to integrate a DDP client.

Keep in mind that this package is calling your Meteor methods and publications at a physical level while exposing a REST-like interface. This means if you have any additional packages that handle roles, authentication, permissions, etc. for your app, those packages should still work just fine over HTTP. This package defines a special publication that publishes a list of all of your meteor methods as rest endpoints. Call it like this:

GET /publications/api-routes

The result looks like:


{ "api-routes": [
  {
    "_id": "/publications/user",
    "methods": [
      "get"
    ],
    "path": "/publications/user"
  },
  {
    "_id": "/methods/print",
    "methods": [
      "options",
      "post"
    ],
    "path": "/methods/print"
  },
}

From the above response, it is clear that publications are accessible with GET, and methods use POST. For example, if I call the print method declared above as rest endpoint it would look like

POST /methods/print
body_param: [ ‘John’ ]

The response would be:-

{
  "code": 200,
  "data": "John"
}

To know how to define and call methods and publications, see the examples here. Once we added this package, it worked well for us, and we were able to call publications and methods as REST endpoints.

However, there were some hiccups. We had various Meteor methods defined such as


Meteor.methods({
    submitTask: function (task) {
        if (Meteor.isServer && Meteor.userId()) {
            const ipAddress = getIpAddress(this.connection);
            try {
                return setTask(task);
            } catch (err) {
                logger.info(err);
                return err;
            }
        }
    }
})

When calling submitTask from the Meteor Client, Meteor.userId() was returning the current userId but it was returning null when calling it as REST endpoint although the user is authenticated.

We were sure that it had something to do with the package that we were using. We looked into the package code and after some research, we found that a function in rest.js is iterating through all of the methods defined on the server(L177), adding them as Http methods(L97), and invoking the methods with arguments coming in the request(L184)(server-initiated Methods). But, as mentioned at (L200) replace with a real one? it meant that the method invocation was fake and we didn't have any idea about what an actual invocation could be ?.

// replace with a real one?
var methodInvocation = {
  userId: userId,
  setUserId: function () {
      throw Error('setUserId not implemented in this ' +
               'version of simple:rest');
  },
  isSimulation: false,
  unblock: function () {
    // no-op
  },
  setHttpStatusCode: function (code) {
     statusCode = code;
  },
};

Solution

So, after going through some references, we found that there was an issue with the Meteor itself which says Meteor.userId() is not accessible inside the server initiated Meteor Methods. And in the same reference, the workaround was also mentioned which is nothing but a way to create the real invocation:-

var invocation = new DDPCommon.MethodInvocation({
  isSimulation: false,
  userId: "myUserId",
  setUserId: setUserId,
  unblock: unblock,
  connection: self.connectionHandle,
  randomSeed: randomSeed
});
DDP._CurrentInvocation.withValue(invocation, () => {
  Meteor.userId(); // "myUserId"
});

This code creates a DDP method invocation object and uses it to invoke a Method. To read more about the meanings of the attributes above, visit here.

So we just replaced the code in rest.js(L200-L222) with this



var invocation = new DDPCommon.MethodInvocation({
  isSimulation: false,
  userId: userId,
  setUserId: function () {
    throw Error('setUserId not implemented in this ' +
                      'version of simple:rest');
  },
  unblock: function () {
  },
  setHttpStatusCode: function (code) {
    statusCode = code;
  },
  connection: req,
});

var result = DDP._CurrentInvocation.withValue(invocation, function () {
  var handlerArgs = options.getArgsFromRequest(req);
    return handler.apply(invocation, handlerArgs);
  });

JsonRoutes.sendResult(res, {
  code: statusCode,
  data: result,
});

Then we built the package and used it in our Meteor app and it worked like a charm. For example, when we called the submitTask Method as a REST Endpoint like this:


POST  /methods/submitTask
Headers: { Authorization: Bearer user-token }
body_param:  [ {
   taskName: “ReadProfileInfo”,
   taskDurationInMilliSec: 200
}]

It returned the response as expected

{
  "code": 200,
  "data": "true"
}

Note:- To use DDP functions we have to include ddp-common in the package.js of simple:rest package and then build it.

Share :