Loopback: Support multiple api versions in a single loopback app

Created on 1 Jul 2016  路  15Comments  路  Source: strongloop/loopback

Kindly add support to multiple api versions in a single loopback app.

For ex:-

In config.*.js, instead of

module.exports = {
  restApiRoot: "/api/v1'',
  host: process.env.HOST || 'localhost',
  port: process.env.PORT || 3000
};

We could do

module.exports = {
  baseUrl: "/api",
  restApiRoots: ["v1'', "v2"],
  defaultApiRoot: "v2",
  host: process.env.HOST || 'localhost',
  port: process.env.PORT || 3000
};

And for a model, say 'device.json', the corresponding js files can be 'device.v1.js' and 'device.v2.js', each file with api's for their respective versions. The acl's in the json file should have api versions included as well.

Suggestions and better way to implement this feature is also welcome

feature stale

Most helpful comment

@joeybenenati sorry, I wasn't complete in my previous comment.

I do the following:

middleware.json:

"routes": {
    "loopback#rest": {
      "paths": [
        "${restApiRoot}"
      ]
    }
  }

In the above case, restApiRoot is the constant you define in config.json eg. restApiRoot: "/api"

Then in your model config.
\

"http": {
    "path": ":apiVersion/user"
  },

The above will give you /api/:apiVersion/user

To grab this variable, you can add a boot script to retrieve it on every API call eg.

var loopback = require('loopback');

module.exports = function(app, bootCallback) {
    // Set the values for apiVersion for all requests coming in
    app.use(function (req, res, next) {
        var matches = req.path.match(/^\/api\/(v\d+)\/.*/i);
        if (matches && matches.length > 1) {
           // this requires some sort of context plugin to provide the apiVersion across execution of this call. Loopback no longer really recommends their context plugin
            req.apiVersion = matches[1];
        }    
        next();
    });

    bootCallback();
};

In your remoteMethod definitions you can explicitly request the apiVersion as a query param eg.

User.remoteMethod (
        'passwordReset',
        {
            http: {path: '/password-reset', verb: 'post'},
            accepts: [
                {arg: 'id', type: 'integer', required: true, http: { source: 'form' }},
                {arg: 'password', type: 'string', required: true, http: { source: 'form' }},
                {
                   arg: 'version', type: 'object', description: 'API version eg. v1, v2, etc.',
                   http: function (context) {
                       return {apiVersion: context.req.apiVersion};
                   } 
                }
            ],
            returns: {arg: 'data', type: 'boolean'}
        }
    );

Incidentally, I use the above refactored into a utility function on all my remote method query param definitions. This way the utility function automatically adds the apiVersion as well as access token to all my param arguments.
Eg.

accepts: ModelManager.buildRemoteMethodArgs([
                {arg: 'id', type: 'integer', required: true, http: { source: 'form' }},
                {arg: 'password', type: 'string', required: true, http: { source: 'form' }}])

It's my solution for the context plugin bugginess observed in loopback.

You can then easily grab it in your remote method function:

User.passwordReset = function(version, id, password, cb) {
        switch ( version.apiVersion ) {
            case 'v1':
               // version specific API method
               break;
            case 'v2':
               // version specific API method
               break;
            default:
               cb(null, false);           
        }
    };

All 15 comments

@vasanthps-rm Would you like to submit a PR? Sounds like a good suggestion. @bajtos @raymondfeng Thoughts?

@superkhau Sure. I will work on it!

@vasanthps-rm :+1: Just ping me when you're done and I'll help review it. Good luck! :four_leaf_clover:

We are looking forward to have this feature :muscle:

+1
But, I will be evil here, should it not be like something as a default building block of an API?

+1

any movement on this?

Any update on this feature?

+1 on this feature.
Currently I'm hacking this by allowing a variable for the API version defined in middleware config:

"routes": {
    "loopback#rest": {
      "paths": [
        "${restApiRoot}"
      ]
    }
  }

Then explicitly looking for that variable in the remoteMethods.

@ako977 could you please elaborate on your versioning workaround? How do you support multiple versions?

@joeybenenati sorry, I wasn't complete in my previous comment.

I do the following:

middleware.json:

"routes": {
    "loopback#rest": {
      "paths": [
        "${restApiRoot}"
      ]
    }
  }

In the above case, restApiRoot is the constant you define in config.json eg. restApiRoot: "/api"

Then in your model config.
\

"http": {
    "path": ":apiVersion/user"
  },

The above will give you /api/:apiVersion/user

To grab this variable, you can add a boot script to retrieve it on every API call eg.

var loopback = require('loopback');

module.exports = function(app, bootCallback) {
    // Set the values for apiVersion for all requests coming in
    app.use(function (req, res, next) {
        var matches = req.path.match(/^\/api\/(v\d+)\/.*/i);
        if (matches && matches.length > 1) {
           // this requires some sort of context plugin to provide the apiVersion across execution of this call. Loopback no longer really recommends their context plugin
            req.apiVersion = matches[1];
        }    
        next();
    });

    bootCallback();
};

In your remoteMethod definitions you can explicitly request the apiVersion as a query param eg.

User.remoteMethod (
        'passwordReset',
        {
            http: {path: '/password-reset', verb: 'post'},
            accepts: [
                {arg: 'id', type: 'integer', required: true, http: { source: 'form' }},
                {arg: 'password', type: 'string', required: true, http: { source: 'form' }},
                {
                   arg: 'version', type: 'object', description: 'API version eg. v1, v2, etc.',
                   http: function (context) {
                       return {apiVersion: context.req.apiVersion};
                   } 
                }
            ],
            returns: {arg: 'data', type: 'boolean'}
        }
    );

Incidentally, I use the above refactored into a utility function on all my remote method query param definitions. This way the utility function automatically adds the apiVersion as well as access token to all my param arguments.
Eg.

accepts: ModelManager.buildRemoteMethodArgs([
                {arg: 'id', type: 'integer', required: true, http: { source: 'form' }},
                {arg: 'password', type: 'string', required: true, http: { source: 'form' }}])

It's my solution for the context plugin bugginess observed in loopback.

You can then easily grab it in your remote method function:

User.passwordReset = function(version, id, password, cb) {
        switch ( version.apiVersion ) {
            case 'v1':
               // version specific API method
               break;
            case 'v2':
               // version specific API method
               break;
            default:
               cb(null, false);           
        }
    };

@vasanthps-rm I just only create a new property (_in my case baseRestApiRoot_) in config*.js like this

module.exports = { baseRestApiRoot: '/api/v1', restApiRoot: '/api/v1.0.5', host: process.env.HOST || 'localhost', port: process.env.PORT || 3000 };

and I modified the middleware.json in this way

"routes": { "loopback#rest": { "paths": [ "${baseRestApiRoot}", "${restApiRoot}" ] } }

and thats all

Any update on this?
I am going to try these work around, but anything official?

@rikantro Can you please explain a bit more on what's happening here?
How do you route models with requests sent to api/v1/model ?
I tried doing what you suggested and now I receive only a 404

Is that feature done?

Was this page helpful?
0 / 5 - 0 ratings