Service-to-service Google API calls with Node

I'm going to walk you through the steps needed to make a service-to-service request from Google API. In this example, I wanted to read Google Product Feeds and create one if necessary. This was all happening on a web server and I did not need access to any users' information, just my own.

This unsurprisingly took some effort to navigate the complexity that is huge API code base. Mostly this is due to the sparse documention in the Google maintainted NPM module googleapis.

So lets get started.

Setting up Google APIs

First thing, getting set up in Google can take some effort. I've taken some screen caps to make things easier. These screen caps are current as of September 2016.

You will first need to create a project in Google API Manager.

Create Project

It will take a few minutes for the project to be created. Once it is created, you will need to enable APIs in your project. Click Enable API.

Enable API

Then select your API. In this example, I'm using Content API for Shopping.
Select API

I'll will be using the Server-to-Server interaction mechanism. This means my webserver will talk directly with Google APIs.

When you click the Enable button at the top, this API will be turned on in the project.

API Enabled

You'll notice that you now have a warning telling you to create security credentials. This means I need grant access to the Google Resources (such as Google Product Feeds) from an account that is tied to this API Project.

Fortunately, Google provides a wizard to help us create the correct credentials. I'm going to connect to Google API via a Web Server and I'm NOT going to be accessing other user data.

Credentials Wizard

Finally, I enter a name for the service account. In this instance, I do not need a role since I will not be managing the API project from a service account.

Service Account

This last process generates a JWT (JSON Web Token) file for the newly created service account. The JWT information will be used in the web server code to make authorized requests to the Google API.

You will also note that the Service Account also has an email address, I'll will use this in a minute.

At this point, Google API is configured. I now need to grant access to Google resrouces.

Setting up the Google Application

The next piece is actually granting the service account access to the Google Application resources it will be working with.

In this example, I'm working with Google Merchants. Logging into Google Merchants I can access the Users via the ellipses next to my User in the top right corner.

Merchants Users

Finally, I a new User and enter the email address of the newly created service account from the previous steps.

Add User

At this point I have granted the service account access to the Google Application data. I'm all set up and ready to write code.

Server-to-Server Google API Example

The code is fairly straightforward. This example leverages the googleapis Node.js module that is maintained by Google.

The API documentation has a lot of detail, most of which is not pertinent to what I'm trying to do here.

First, a note about these examples... this code works with Node6 using Babel + async/await. If you're using Babel, I hightly recommend you do. It makes writing JavaScript a pleasure. Guillermo Rauch has a nice Gist on configuring a project with this functionality in the lightest way possible.

Creating a JWT Client

The first piece of code will generate a client. Since I'm doing server-to-server connections and have downloaded the service account's JWT file, I will use the JWT client provided in the googleapis module.

The API that I'm connecting to, List Feeds API will contain the "scope" of the authorization needed to be granted. This will likely be different depending on the Google APIs you will be accessing.

In this case it's

https://www.googleapis.com/auth/content  

I'll use this scope when I construct the client. An example connection looks like this:

import serviceAccountJwt from './service-account-jwt.json';

function connect() {  
  return new Promise((resolve, reject) => {

      // scope is based on what is needed in our api
    const scope = [ 'https://www.googleapis.com/auth/content' ];

    // create our client with the service account JWT
    const { client_email, private_key } = serviceAccountJwt;
    const client = new google.auth.JWT(client_email, null, private_key, scope, null);

    // perform authorization and resolve with the client
    client.authorize((err) => {
      if(err) reject(err);
      else    resolve(client);
    });
  });
}

First thing I'm doing is wrapping this process in a Promise. I'm doing this so I can use async/await in subsequent methods.

You'll see that I use the JWT's file information to create a new google.auth.JWT client.

Then authorize is called on the client and I either resolve or reject the Promise based on the results of the authorize invocation.

Now that I can create an API client, I can use it to make requests!

Get Requests

The first thing I'll do is read some data. I can leverage the connect method I just created.

The method we're going to call is for listing datafeeds.

You'll notice that this API is version v2. One of the confusing pieces of the documentation for googleapis is how to conenct to the different APIs. They actually tell you just not very clearly.

So what you need to do is navigate to Google APIs Explorer and find the API that you want to access. In this example it's Content API for Shopping v2.

Armed with this knowledge, I create an appropriate endpoint. Looking on that page I see that the listing of datafeeds is called

content.datafeeds.list  

This means I will need to call a method called content and since it's the v2 version I'll pass that as the argument, like so...

google.content({ version: 'v2' })  

If we wanted to access v3 of the Analytics API, it will be...

google.analytics({ version: 'v3' })  

Now for the rest of the example:

function getFeeds(client, merchantId) {  
  return new Promise((resolve, reject) => {

    // generate the endpoint
    const endpoint = google.content({ version: 'v2', auth: client });

    // call the API method and pass required / optional
    // parameters as the first argument
    endpoint.datafeeds.list({ merchantId, maxResults: 250 }, (err, lists) => {
      if(err) reject(err);
      else    resolve(lists.resources);
    });
  });
}

I again wrap this method in a Promise so it can be used with async/await.

I also create the endpoint and supply a client that has been authorized.

Lastly, I call the actual API method content.datafeeds.list and supply the call with the required and optional parameters listed in the API reference. In this case, the merchantId is required and the maxResults is optional.

I now have a get request implemented. The next step is creating some data.

Insert and Update Requests

The last piece of the puzzle is inserting/updating data with API calls. This is remarkably similar to the get request, however there is one piece of information you must work through to get things working.

That is you need to actually send the required information in the resource property of the parameters you pass in.

So for this example, I'll be inserting a new feed, using the insert feed API. This method has a URL parameter of merchantId, just like the get request.

It also expects a BODY object that forms to the datafeeds resource. My method will take the appropriate information needed to construct this resource and then pass it to the API call as the resource property on the submitted paramters.

function insertFeed(client, merchantId, feedName, fileName) {  
  return new Promise((resolve, reject) => {

    // construct params with merchantId at root
    // and resource that contains properties 
    // of the datafeeds resource
    const params = {
      merchantId: merchantId,
      resource: {
        contentLanguage: 'en',
        contentType: 'products',
        fileName: fileName,
        name: feedName,
        targetCountry: 'US',
        attributeLanguage: 'en',      
        intendedDestinations: [ 'Shopping' ],
        format: { quotingMode: 'value quoting' },
      },
    };

    // construct the endpoint
    const endpoint = google.content({ version: 'v2', auth: client });

    // perform the insert API call
    endpoint.datafeeds.insert(params, (err, feed) => {
      if(err) reject(err);
      else    resolve(feed);
    });
  });
}

Again this method returns a Promise to allow usage of async/await.

After creating the parameters to the request, with the datafeed information inside the resource property, I create an endpoint.

Finally, the API call is made and the newly created datafeeds resource is used in the Promise resolution.

Now that I have some hepler methods for interacting with Google APIs, I need to use them...

Putting it all together

Our last method is an async function that will combine the three helpers that have been created.

The example below will "upsert" a feed by first searching for a feed that has the same name. If it doesn't find the feed, it will be created.

async function upsertFeed(merchantId, feedName, fileName) {

  // create the client and authorize
  let client = await connect();

  // look for a feed that has the same name
  let feeds = await getFeeds(client, merchantId);
  let foundFeed = feeds.find((feed) => feed.name === feedName);

  // return if we found it
  if (foundFeed)
    return foundFeed;

  // create the feed if we haven't already
  return await createFeed(client, merchantId, feedName, fileName);
}

This method will first connect and authorize with the service account.

Then the feeds will be loaded and we will search to find one that matches the name that we supplied.

If no feed is found, it will create the feed.

That's all there is to it! I hope this example helps you get up and runing with Google APIs.

Full Example

import google from 'googleapis';  
import serviceAccountJwt from './service-account-jwt.json';

export default {  
  upsertFeed,
};

// Helper functions
/////////////////////////////////////////////////////

function connect() {  
  return new Promise((resolve, reject) => {

      // scope is based on what is needed in our api
    const scope = [ 'https://www.googleapis.com/auth/content' ];

    // create our client with the service account JWT
    const { client_email, private_key } = serviceAccountJwt;
    const client = new google.auth.JWT(client_email, null, private_key, scope, null);

    // perform authorization and resolve with the client
    client.authorize((err) => {
      if(err) reject(err);
      else    resolve(client);
    });
  });
}


function getFeeds(client, merchantId) {  
  return new Promise((resolve, reject) => {

    // generate the endpoint
    const endpoint = google.content({ version: 'v2', auth: client });

    // call the API method and pass required / optional
    // parameters as the first argument
    endpoint.datafeeds.list({ merchantId, maxResults: 250 }, (err, lists) => {
      if(err) reject(err);
      else    resolve(lists.resources);
    });
  });
}

function insertFeed(client, merchantId, feedName, fileName) {  
  return new Promise((resolve, reject) => {

    // construct params with merchantId at root
    // and resource that contains properties 
    // of the datafeeds resource
    const params = {
      merchantId: merchantId,
      resource: {
        contentLanguage: 'en',
        contentType: 'products',
        fileName: fileName,
        name: feedName,
        targetCountry: 'US',
        attributeLanguage: 'en',      
        intendedDestinations: [ 'Shopping' ],
        format: { quotingMode: 'value quoting' },
      },
    };

    // construct the endpoint
    const endpoint = google.content({ version: 'v2', auth: client });

    // perform the insert API call
    endpoint.datafeeds.insert(params, (err, feed) => {
      if(err) reject(err);
      else    resolve(feed);
    });
  });
}

// Exports
///////////////////////////////////////////////

async function upsertFeed(merchantId, feedName, fileName) {

  // create the client and authorize
  let client = await connect();

  // look for a feed that has the same name
  let feeds = await getFeeds(client, merchantId);
  let foundFeed = feeds.find((feed) => feed.name === feedName);

  // return if we found it
  if (foundFeed)
    return foundFeed;

  // create the feed if we haven't already
  return await createFeed(client, merchantId, feedName, fileName);
}
comments powered by Disqus