@classroomtechtools/endpoints

A library SDK for Google AppsScripts that makes working with API endpoints a cinch. You can use it as an abstraction of UrlFetchApp, or use it to wrap Google APIs that don't have advanced services yet.

Usage no npm install needed!

<script type="module">
  import classroomtechtoolsEndpoints from 'https://cdn.skypack.dev/@classroomtechtools/endpoints';
</script>

README

Endpoints

A library SDK for Google AppsScripts that makes working with API endpoints a cinch. You can use it as an abstraction of UrlFetchApp, or use it to wrap Google APIs that don't have advanced services yet.

See the documentation.

Example Libraries that use Endpoints:

Quickstart

Install:

  • Library ID: 1WovLPVqVjZxkxkgCNgI3S_A3eDsX3DWOAoetZgRGW1JpGQ_9TK25y7mB

Use instead of UrlFetchApp.fetch:

function myFunction () {
  // create simple get request with query parameter
  const json = Endpoints.get('https://example.com', {
    query: {
      s: 'searchstring'
    }
  });

  // create simple post request with headers and payload
  const json = Endpoints.post('https://example.com', {
    payload: {
      key: 'value'
    },
    headers: {
      'Authorization': 'Basic ' + token;
    }
  });
}

Use it to interact with Google APIs:

function myFunction () {
  // creates endpoint with oauth as "me"
  const endpoint = Endpoints.createGoogEndpoint('sheets', 'v4', 'spreadsheet.values', 'get');

  // use endpoint object to create get or post requests
  const request = endpoint.httpget({spreadsheetId: '<id>'});

  // the request object has fetch method
  const response = request.fetch();

  // the response object has json property
  Logger.log(response.json);
}

Or use it to programmatically create different kinds of requests:

function myFunction () {
  const request = Endpoints.createRequest('get', 'https://example.com');
  if (condition)
    request.addQuery({s: 'searchstring'});
  else
    request.addQuery({p: 'something'});
}

Or really get into the weeds. Looking at reference documentation, we can derive this:

function myFunction () {

  // build it more manually "from scratch" with `createRequest`
  const request = Endpoints.createRequest('put', {
    url: 'https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}',
    spreadsheetId: '<id>',
    range: '<range'
  }, {
    query: {
      valueInputOption, 'USER_ENTERED'
    },
    payload: {
      'values': [
        [1, 2, 3]
      ]
    }
  });


  const json = request.fetch().json;
}

Examples

Download from Wikipedia

Wikipedia offers a great API to get us started learning how to use this library to make simple requests.

const wpurl = 'https://test.wikipedia.org/w/api.php';
const request = Endpoints.createRequest('get', wpurl);

The documentation for this API says we have to provide it some standard query parameters, so that there's an ?action=query and &format=json tacked onto the end of the URL, so let's figure out how to add them.

We have a request object and we explore the Request class which indicates there is an addQuery method.

const query = {action: 'query', format: 'json'};
request.addQuery(query);
const url = request.url;
Logger.log(url);  // url includes the query parameters

The target API also requires a titles query parameter that indicates which article we are after:

request.addQuery({titles: 'Albert_Einstein'});
request.url;  // the full url including previous query parameters

Let's reach out to the internet and get the response:

const response = request.fetch();
if (!response.ok) throw new Error(`Status code ${response.statusCode}`);
const json = response.json;
Logger.log(json);  // the response as json

Interact with Google APIs, in batch mode

Let's get real. We probably don't use Google AppsScripts to download wikipedia articles. We use it to build applications that depend somehow on Google APIs, such as the Spreadsheet API.

Imagine your application needs to write the exact same information to two different spreadsheet documents. Let's learn how to do that with batch operations.

const endpoint = Endpoints.createGoogEndpoint('sheets', 'v4', 'spreadsheets.values', 'update');  

Under the hood, this createGoogEndpoint uses the Discovery Services API to build the endpoint object.

In this case, we are interacting with the "sheets" service, version "v4", in the resource "spreadsheets.values" and the "update" method, which is the target endpoint that Google exposes for updating spreadsheets that we have in mind. We need to create a http put request with that endpoint. Documentation for this is here.

Now that we have an endpoint object, by looking through the Endpoints namespace, we find that httpput method which will allow us to prepare the request object:

const request = endpoint.httpput(
  {    /* path parameters */		
    spreadsheetId: '<id>',
    range: 'Sheet1!A1'
  },{  /* request body */
    query: {
      valueInputOption: 'RAW'
    }
    payload: {
      range: 'Sheet1!A1',
      majorDimension: 'ROWS',
      values: [[1]]
    }
  }
);

const response = request.fetch();
Logger.log(response.json);  // got data

So the general sequence in this library is that you make an Endpoint instance, then on that use a method that starts with http to get a Request instance, call .fetch on that to get a Response object, and call json on that to get the raw data back.

That's how to do one, but how to do two programmatically? Let's use the batch functions. Imagine you need to update two different spreadsheets with the exact same info:

const batch = Endpoints.batch();
const ids = ['<id1>', '<id2>'];
    
ids.forEach(id => {
  const request = endpoint.httpput({
    range: 'Sheet1!A1',
    spreadsheetId: id
  }, {
    query: {
      valueInputOption: 'RAW'
    },
    payload: {
      range: 'Sheet1!A1',
      majorDimension: 'ROWS',
      values: [[1]]
    }
  });

  batch.add({request: request});

});
    
const responses = batch.fetchAll();
for (const response of responses) {
  Logger.log(response.json);
}

You don't have to use the batch functions to create requests to the same endpoint. Any request that you can create with this library can be processed in batch mode, as long as the request parameter to add is a Request object.

When working in batch mode, it's most convenient to add properties to the individual requests. Adding requests supports the very last parameter being an objects, whose keys are added to the request object.

const batch = Endpoints.batch();
// we want to keep name on the request object itself:
const items = [{id: '<id1>', name: 'a'}, {id: '<id2>', name: 'b'}];
    
ids.forEach([id, name] => {
  const request = endpoint.httpput({
    range: 'Sheet1!A1',
    spreadsheetId: id
  }, {
    query: {
      valueInputOption: 'RAW'
    },
    payload: {
      range: 'Sheet1!A1',
      majorDimension: 'ROWS',
      values: [[1]]
    }
  }, {  // last parameter
    name  // <— it'll be on the request object
  });

  batch.add({request: request});
});

batch.fetchAll().forEach(request => {
  Logger.log(request.name);  // name is here
});

Batch mode: Bring on the heavy

Above, we see that this library allows the developer to create request objects that interact with APIs. You can make two calls at once. But what about if you have lots of API calls to make, like 1000s of them? Maybe you want all your students from an SIS on a google spreadsheet, or maybe you want to get a bunch of calendar event info for an event.

If you were to do this with UrlFetchApp.fetch you'll probably run out of execution time. That's why batch mode is so useful, as you get into the fast territory.

However, when you get to be that efficient, API endpoints always have rate limitations that a program needs to respect. You get a 429 error when you've gone too fast, and then you have to wait until you can try again. (It's annoying but necessary to ensure scalability.) That's a pain to program, but this library abstract that away from you … with an iterator!

Look how easy it is to go through 1000s of API calls:

Suppose that your API for news articles has an id for the path parameter, and you want to get the first 1000 articles. Let's build it:

const batch = Endpoints.batch(50);   // 50 is default 
for (let id= 1; id <= 1000; id++) {
  const request = Endpoints.createRequest('get', {id});
  batch.add({request});
}

At this point, if you were to just do batch.fetchAll() it would attempt to download all 1000 articles, and wouldn't give up until it was done. Then you could do what you needed with those articles.

Instead, let's download them in chunks, passing time back to you every now and again, so that you can do the necessary processing. Then it can try and get the next chunk for you. That way, it's easier for the library to juggle the huge number of requests as it's inserting a nice breathing space for the rate limitation to "reset."

We can do that with an iterator!


for (const response of batch) {
  Logger.log(response.json);
}

The above is a bit magical and doesn't show you all the stuff happening underneath, but it's actually calling fetchAll for articles 50 at a time (the default rate limit, but you can set it higher), and then spits them out to you one at a time.

That's it!

While this iterator pattern does its best not to cross the passed-in rate limitation, if a rate limit is encountered anyway, it is also handled for you. First it pauses for as long as the response code tells it to, and then does a manual round trip, returning that response.

Notes on createGoogEndpoint

This method accepts four parameters name, version, resource and method, and these four have to match how the Discovery Service is structured and kept. The best way to get the real values that are really needed (for now?) is to go to this try API page and try to work out the exact terminology for the four parameters.

Reference for commonly-used discovery documents

Finding these names can be tricky, which is why this table can be helpful:

Common Name name, version Documentation Link
Admin Directory "admin", "directory_v1" link
Admin Reports "admin", "reports_v1" link
Sheets "sheets", "v4" link

If you, like me, have to spend a few minutes finding them, add a pull request and I'll add it to the table.

To import as an npm module:

npm install @classroomtechtools/endpoints

The above will also install the following dependencies:

  • @classroomtechtools/unittesting
  • @classroomtechtools/enforce_arguments

Unit tests

This library was built with unit tests, both locally and in the AppsScripts context.

interacting incorrectly with endpoint that produces 404
  ✔ json instead contains error object with message and information

using batch mode to mirror spreadsheet writes
  ✔ responses has length of 2

using service account to create
  ✔ Spaces available

http get requests to sheets v4 service, expected to fail
  ✔ Endpoint#baseUrl returns templated string of endpoint
  ✔ Request#getUrl returns url based on substitutions within baseUrl
  ✔ Response#isOk indicates unsuccessful request
  ✔ Response#statusCode indicates 403 error (permission denied)
  ✔ Response#headers returns headers with Content-Type 'application/json; charset=UTF-8'
  ✔ Response#json returns json with error.status set to 'PERMISSION_DENIED'
  ✔ Response#response returns the original request
  ✔ Request#headers returns Authorization: Bearer

http get request with no authentication
  ✔ internally, Request#query object starts with empty object
  ✔ internally, Request#url is same as original passed
  ✔ Request#addQuery appends query parameters to returned url
  ✔ Request#addQuery appends query parameters to url, keeping old values
  ✔ Response#ok returns true on success
  ✔ Response#json returns parsed json
  ✔ Response#statusCode returns 200 on success