The LoopBack REST Connector and its secrets

October 28, 2016Sammy Teillet10 min read

thumbnail

I had a lot of trouble using the Loopback REST connector reading the existing documentation and I'm sure you'll have less issues by reading this first.

Summary

This article will show how to:

  • Use API methods from your server
  • Use environment variables for production
  • Get values from the headers of a response
  • Customize the error handling of the connector

A Loopback connector connects a Loopback datasource to your DB (mongo, postgreSQL, etc) or to a REST API.

It can be very useful when you want to separate your logic in microservices, or if you want to access an API from the server.

Get started

The Loopback-connector-rest works this way:

  • Create a model MyAPI that will contain all the methods that use the API
  • Your model is connected to a dataSource APIDataSource
  • This dataSource is connected to your API through the Loopback-connector-rest

Add the model

In your model-config.json add the following:

"MyAPI": {
  "dataSource": "APIDataSource",
  "public": true
}

and in your common/models/MyAPI.json:

{
  "name": "myAPI",
  "plural": "myAPI",
  "base": "Model",
  "idInjection": false,
  "properties": {},
  "options": {
    "promisify": true
  },
  "acls": [],
  "methods": []
}

Create your dataSource

in your datasource.json

"APIDataSource": {
  "connector": "rest",
  "name": "restAPI",
  "operations": []
}

At this point your model myAPI contains nothing.
Your server can't even start until you've installed the Loopback-connector-rest.

npm install loopback-connector-rest --save

Alright you are ready to build your first method!

At first, I thought that I would need a file with module.exports = (MyAPI) -> to contain all my methods.
No such things with the REST connector.
I would clearly advise not to write any methods in this model so it is exclusively designed to call the API end-point and nothing else.
Whenever you need to add some logic, do it in your others models.

The API

To start playing with an API, you need an API. You can install this dumb-api.

Once you've installed the dumb-api, launch the server, take a look at it http://0.0.0.0:3000/explorer/#.
There are a lot of methods, we will focus on the main four, the CRUD (Create, Read, Update, Delete):

  • POST /stuff
  • GET /stuff
  • PUT /stuff/{id}
  • DELETE /stuff/{id}

The GET method

The GET is the easiest one.
What you want to do is to call the getStuff method of our API from another model OtherModel and log the result in the console.
This should look like the following:

OtherModel.app.models.MyAPI.getStuff()
.then(function (result) {
  console.log(result)
  })

At this point, this won't do anything because MyAPI.getStuff doesn't exist.

Let's create it.
How would you usually export a method on a Loopback model?

module.exports = function(MyAPI) {
  return MyAPI.getStuff = function() {
    return stuff;
  };
};

Well, you won't write any of this... Delete it! The Loopback-connector-rest writes the methods itself, with a little bit of configuration.

The Loopback-connector-rest works with operations. An operation contains two keys, the first being:

  • The functions: it contains your methods and describes the signature of your methods

So let's add a function in the datasources.json by creating a new operation.

"APIDataSource": {
  "connector": "rest",
  "name": "restAPI",
  "operations": [
    {
      "functions": {
        "getStuff": []
      }
    }
  ]
}

Now you have a method of MyAPI that takes 0 arguments.

What does this function return?
It returns the body of the response of the request you call.
You define this request in the second key of the operation: the template.

You need:

  • A method: GET, POST, etc
  • A url: Click on try it out on the explorer, the url is in 'Request URL'
"APIDataSource": {
  "connector": "rest",
  "name": "restAPI",
  "operations": [
    {
      "functions": {
        "getStuff": []
      },
      "template": {
        "method": "GET",
        "url": "http://0.0.0.0:3000/api/Stuff"
      }
    }
  ]
}

And that's it, now when you call OtherModel.app.models.MyAPI.getStuff() you will get a promise with the response body of the request!

And the response will be an empty array... That's because there is no stuff yet in your dumb-api. Let's create one.

The POST method

You want to be able to create new stuff from our server.

var newStuff = {
  id: 1,
  thing: 'my new stuff'
};
OtherModel.app.models.MyAPI.postStuff(newStuff)

Let's create a new operation with a function called postStuff using one argument newStuff.

{
  "functions": {
    "postStuff": ["newStuff"]
  }
}

Then you configure the request by adding a body.

{
  "functions": {
    "postStuff": ["newStuff"]
  },
  "template": {
    "method": "POST",
    "url": "http://0.0.0.0:3000/api/Stuff",
    "body": "{newStuff}"
  }
}

For safety reasons, you can specify the type of this new argument which is an object.

"body": "{newStuff:object}"

Or, to detect easily why the requests wouldn't work, you can specify each of the parameters of the POST

{
  "functions": {
    "postStuff": ["newStuffId", "newStuffThing"]
  },
  "template": {
    "method": "POST",
    "url": "http://0.0.0.0:3000/api/Stuff",
    "body": {
      "id": "{newStuffId:integer}",
      "thing": "{newStuffThing:string}"
    }
  }
}

Finally call the method with:

OtherModel.app.models.MyAPI.postStuff(1, 'my new stuff')

PUT and DELETE methods

For the PUT method you need the id inside your URL.

{
  "functions": {
    "putStuff": ["updatedStuffId", "updatedStuffThing"]
  },
  "template": {
    "method": "PUT",
    "url": "http://0.0.0.0:3000/api/Stuff/{updatedStuffId:integer}",
    "body": {
      "id": "{updatedStuffId:integer}",
      "thing": "{updatedStuffThing:string}"
    }
  }
}

To give you an example, if you need to be authenticated to your API for a DELETE, you might need to add a token and a userId in the header of your request.

{
  "functions": {
    "deleteStuff": ["userId", "token", "stuffId"]
  },
  "template": {
    "method": "DELETE",
    "url": "http://0.0.0.0:3000/api/Stuff/{StuffId:integer}",
    "headers": {
      "authentication": "{token:string}",
      "userId": "{userId:string}"
    }
  }
}

Use the Debug

When developing with the Loopback-connector-rest, the requests and responses are quite magic.
If you want to monitor the activity of the connector use the debug mode.

bash
DEBUG=* node server/server.js

If you want to display only the logs from the REST connector:

bash
DEBUG=*rest node server/server.js

Environment variables for production

For different environments you might have different urls for your API.
The datasources.local.js file enables to use local environment variables to configure your connectors.

For instance, on your dev machine we use http://0.0.0.0:3000/api/ and on the production http://prod.api:3000/api/.

You should export this on your production machine:

bash
export LOCAL_API_URL=http://prod.api:3000/api/

in your datasource.local.js file, every url should look like this:

url: (process.env.LOCAL_API_URL || "http://0.0.0.0:3000/api/") + "Stuff/{StuffId:integer}"

And if you have an API version, you should separate the url and the version:

bash
export LOCAL_API_URL=http://prod.api:3000/
export LOCAL_API_VERSION=api/v1.0/
url: (process.env.LOCAL_API_URL + process.env.LOCAL_API_VERSION || "http://0.0.0.0:3000/api/") + "Stuff/{StuffId:integer}"

Access the headers of a request

The Loopback-connector-rest works fine returning the body of the response.
But when it comes to access other part of the response, it gets tricky.

For instance, in my case, I wanted to update a list of stuff after a POST request.
The request to get the whole list of stuff was really long, so the only way for us was to get the last, newly created item.
In most cases, the POST request returns the new item and the connector works fine.
The API I was working with responded with a code 200 and an empty body after a successful creation.
However, they where sending the location of new item in the response's headers, where the id can be found.
"location": "http://0.0.0.0:3000/api/Stuff/stuffId"

So I needed to access the request's headers and put the stuffID it in the body to be able use it.

Hooks

If you are a Loopback user you might have heard of the operations hooks.
Hooks are functions that are triggered every time an operation is done, such as:

  • Before a CREATE operation
  • After a DELETE

I quote the documentation:

A typical request to invoke a LoopBack model method travels through multiple layers with chains of asynchronous callbacks. It's not always possible to pass all the information through method parameters.>

A hook gives you access to the Loopback context ctx of the operation which contains a lot of information that won't make it to the end of your method.
Typically: in the end of a call you get the body of the response, the headers are only available in the ctx.

In many case the hook is the .observe method of the model.
In our case this won't work.
You have to use the hook of the connector itself.
And where do you access the connector object? During the boot!

So you are going to need a new file in the boot.

in server/boot/set-headers-in-body.js

module.exports = function(server) {
  var APIConnector;
  APIConnector = server.datasources.APIDataSource.connector;
  return APIConnector.observe('after execute', function(ctx, next) {

  });
};

Now inside this function you have access to every information you need:

  • the headers of the response: ctx.res.headers
  • the code of the response: ctx.res.body.code
  • the method of our request: ctx.req.method

Remember, the hook is called every time the operation is done, the operation is a call to the connector, so each of your requests with the rest-connector will go through this function.

If you try the code now, every call to the API won't go through this hook because you are not calling the next() function yet.

You want to hook every POST request our server does and put the location from the header inside the body.

module.exports = function(server) {
  var APIConnector;
  APIConnector = server.datasources.APIDataSource.connector;
  return APIConnector.observe('after execute', function(ctx, next) {
    if (ctx.req.method === 'POST') {
      ctx.res.body.location = ctx.res.headers.location;
      return ctx.end(null, ctx, ctx.res.body);
    } else {
      return next();
    }
  });
};

The ctx.end method needs 3 arguments: (err, ctx, result). The result is what is send in the end, when the method of MyAPI is called.

Warning: be careful with hooks.
With the code you just wrote, the connector never sends any errors after a POST request for instance.

A way to avoid most bugs is to specify in which case you touch the ctx:

if (ctx.req.method === 'POST' && ((ref = ctx.res) != null ? (ref1 = ref.body) != null ? ref1.code : void 0 : void 0) === 200 && ctx.res.headers.location)

Error handling

There can be many use of hooks, such as formatting every response you get from the API, very usefull when you work with an API that returns xml for instance.
Or handle errors from the API before it comes to your model inside your promise.

In my last project, the API was a gateway, so every error they sent was a HTTP error 502 Bad Gateway when it should have been 403 Forbidden.
In order not to be confused with our server errors and the gateway errors, we customized the error rejection in the hook.

module.exports = function(server) {
  var APIConnector;
  APIConnector = server.datasources.APIDataSource.connector;
  return APIConnector.observe('after execute', function(ctx, next) {
    var err, ref, ref1;
    if (/^[5]/.test((ref = ctx.res) != null ? (ref1 = ref.body) != null ? ref1.code : void 0 : void 0)) {
      err = new Error('Error from the API');
      err.status = 403;
      err.message = ctx.res.body.message;
      return ctx.end(err, ctx, ctx.res.body);
    } else {
      return next();
    }
  });
};

Conclusion

I wanted to share the knowledge I acquired using this connector with a tutorial.
I'm using Loopback v2.22.2, if you had trouble understanding/implementing any part of this article or if you have some improvement to suggest, please contact me through the comments!

And remember: always use the DEBUG=*rest when you work with the connector!

S

Sammy Teillet

Web Developer at Theodo