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.
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
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 useMyService._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;