Using External APIs

The recommended method of interacting with external APIs is using the ApiConsumer class. The ApiConsumer is a superclass that helps you in creating services that are mainly about consuming data from a third-party provider. ApiConsumer provides several helpers and automatically handles processes such as:

  • Caching external calls to third-party providers

  • Naming the endpoint

  • Receiving and handling image buffers

  • Artificially slowing down message sending to the user

  • Automatically aggregating results from multiple endpoints

When exporting an ApiConsumer subclass, any methods added to it that are not prefixed with an underscore (_) will be available as calls to the created RPC.

Concepts

There are two important concepts worth explaining.

  1. Query Options: is an object or an array of objects (if you want aggregated results from multiple endpoints) including instructions about the request to the server. it is then used by _requestData or _requestImage to query the provider.

queryOptions = {
queryString: the unique query string for this request
baseUrl: {string} representing the base url of the target api
method: {string} html action: 'GET', 'POST', etc
body: if posting, this is the place to indicate the body
headers: {object} a key value dictionary of headers
json: {boolean} to indicate if the response is json or not. default: true
  1. Parser Functions: these are functions provided by the user of the class that should transform, filter, aggregate, and clean the response from the provider to simple (key value pairs with max depth of 2), understandable JSON objects presentable to NetsBlox users. Different methods in ApiConsumer expect such functions.

Methods

Here are a few helpers to get you started with using ApiConsumer

  • _sendStruct - queries the provider and sends the response in form of a list of structured data to the caller. _sendMsgs: same as _sendStruct but sends messages back instead of returning a single list.

  • _sendImage - used for sending images that can be used as costumes to the caller.

  • _sendAnswer - use this to send a single answer from a query to the user. For example if there is an endpoint which returns information for a car and you are making a method that only returns the model of the car you can use MyService._sendAnswer(QueryOpts, '.model')

  • _stopMsgs - stops sending of the queued messages.

Some of the underlying methods could also be directly used to further customize the response.

  • _requestData - fetches text data.

  • _requestImage - fetches and image and makes it available as buffer to be sent to user.

API Keys

Definition

If a service requires an API key which isn’t already defined, the API key type should be defined here. An API key should provide a human-readable name and help URL. The default value is preferred for the environment variable and can only be overridden for ensuring backwards compatibility.

Registration

The service can register API keys as shown below.

ApiConsumer.setRequiredApiKey(GoogleMaps, GoogleMapsKey);

The API key can then be accessed via this.apiKey.value within the individual RPCs. This ensures that a user’s custom API key will be used when possible.

Error Handling

In the event of an invalid custom API key, the user should be notified in a consistent manner. To this end, it is important to throw an InvalidKeyError in the event of an “unauthorized” error by the given service. Although this should be addressed by default using status codes, not all APIs handle these errors in the same way.

 1// service description
 2// api docs = https://jsonplaceholder.typicode.com/
 3const ApiConsumer = require('../utils/api-consumer');
 4const {SomeApiKey} = require('../utils/api-key');
 5
 6// provide the service name and api's base url to the constructor
 7SampleConsumer = new ApiConsumer('test', 'https://jsonplaceholder.typicode.com');
 8ApiConsumer.setRequiredApiKey(SampleConsumer, SomeApiKey);
 9
10/**
11* returns all available posts up to a limit
12* @param {Number} limit limit on how many posts to return
13* @returns {Object} list of posts
14*/
15SampleConsumer.getPosts = function(limit) {
16    const queryOpts = {
17        queryString: '/posts',
18        headers: {
19            'auth-header': API_KEY
20        }
21    };
22
23    const respParser = function(response) {
24        // validate the response from the server
25        if (!Array.isArray(response)) throw new Error('response is not an array');
26        let posts = response
27                // filter some of the results
28                .filter(post => {
29                    if (!post.title.toLowerCase().includes('word')) return true
30                })
31                // limit the response size
32                .slice(0,limit)
33                // mutate the results to keep what you need and also simplify the structure
34                .map(post => {
35                    delete post.id;
36                    delete post.userId;
37                    return post;
38                })
39        return posts;
40    }
41
42    return this._sendStruct(queryOpts, respParser);
43};
44
45module.exports = SampleConsumer;