How to Use the Google Drive API with JavaScript

Node.js, Authentication and Google’s Drive API

Published on
Jun 3, 2019

Read time
8 min read

Introduction

Google has a huge collection of APIs, and sooner-or-later in your development career, you’re likely to encounter one or more of them: from Google Maps to Google Calendar, YouTube to Google Analytics, and Gmail to Google Drive — there are APIs for a wide variety of tools in Google’s large product-base.

However, the scale of the documentation and the sheer number of available options can make it difficult for newer developers to get started with these tools. Plus, handling authentication can be tricky: there are multiple options depending on the tools you’re using and the context you’re using them in.

In this article, we’ll look into how to use the Google Drive API with JavaScript. If you want to code along to this article, make sure you have Node.js installed. Then, create a new directory for your project, navigate to it in the terminal and type npm init -y. Finally, create a file called index.js in the root folder of your new project.

Authentication

Before using any Google APIs, we’ll need to get past authentication. Visit console.developers.google.com and select a project or create a new one. Projects are used so that it’s easy to see any API activity and billing for a particular app or business in the same place, and you can access ‘credentials’ using the tab on the left.

(Unless you’re making a large amount of API calls, you won’t need to worry about billing. Google offers a generous amount for free, and this tutorial will keep you well within the limit!)

Option 1: API Key

This is the simplest option to authenticate, as it’s just a single string. But it’s also the least secure, which means that it isn’t enough for certain applications, and so we won’t worry about API keys in this article.

Option 2: OAuth 2.

If you need users to provide permission to your app, you can use the OAuth 2.0 standard. This will ask users to authorize your app before you’ll be able to make changes using any APIs. This is a necessity for front-end applications and certain APIs (such as the Gmail API). But for our needs — a server-side application using the Google Drive API — it’s unnecessary.

Option 3: Service Accounts

The final option is a service account: a special type of Google account used to represent a non-human user. This is the best solution for us to control Google Drive using a server-side app and so this is the option we’ll use.

So, go to the Credentials page, click “create credentials” and then click “service account key”. When you’re done, you’ll get a JSON file with the following structure:

{
  "type": "service_account",
  "project_id": "",
  "private_key_id": "",
  "private_key": "",
  "client_email": "",
  "client_id": "",
  "auth_uri": "",
  "token_uri": "",
  "auth_provider_x509_cert_url": "",
  "client_x509_cert_url": ""
}

Name this file credentials.json and move it to the root folder of your project.

Enable APIs

Once you’ve done that, you should enable the APIs you want to use in Google’s API Library. By default, nothing is enabled, to prevent you from changing anything by accident: for the purposes of this article, you’ll want to enable Google Drive and Google Sheets.

Share Files

To use the service account method of authentication, you’ll need to provide access to the service account. You can provide access on a file-by-file basis or at the level of folders. Find the "client_email" key in your credentials.json file and copy the value. This is the email address that you’ll need to share, and it will look something like:

service-account@projectname-123456.iam.gserviceaccount.com

Create a new folder in your account’s Google Drive, and then share it with the "client-email" in your credentials.json. Sharing access to the service account is required, and it’s one of the reasons a service account is more secure than a simple API key.

Install Modules

Finally, before we begin coding, we’ll need to install the Google APIs package in our project. Open the terminal, making sure you’re in the project directory, and type npm i googleapis. The conventional way to import the module into a Node.js file is like this:

const { google } = require("googleapis");

We can now start coding!

Getting Google Drive API Up and Running

Scopes and Authorisation

One of our first tasks in the code is to specify which scopes we want to use. You can get a full list of scopes here. For our purposes, we’ll want the following scope:

const scopes = [
  "[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/drive)",
];

We can import the relevant credentials from out credentials.json file, and so we now have everything we need for authorisation.

const { google } = require("googleapis");
const credentials = require("./credentials.json");

const scopes = [
  "[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/drive)",
];

const auth = new google.auth.JWT(
  credentials.client_email,
  null,
  credentials.private_key,
  scopes
);

const drive = google.drive({ version: "v3", auth });

We’re using google.auth.JWT to create a JSON Web Token object, and we’ll pass this as an argument when we call the Drive API.

Testing the Code

Before looking at some more complex features, let’s check that our code is working correctly with a simple example. Using the drive.files.list method, we can see every file our service account has access to.

drive.files.list({}, (err, res) => {
  if (err) {
    throw err;
  }

  const files = res.data.files;

  if (files.length) {
    files.map((file) => {
      console.log(file);
    });
  } else {
    console.log("No files found");
  }
});

To begin with, we’re using the default arguments, which is why the first argument of our function is an empty object. To run the file, type node . into the terminal. If everything’s working, you should see one or more JSON objects or No files found.

Adding Arguments

To see all the possible request parameters, it’s best to refer to the documentation. One of the most useful parameters is ‘fields’, and to see all the available fields, pass this object in as the first argument of drive.files.list:

{
  pageSize: 1,
  fields: '*',
}

If you have at least one document, it will show you every available field for that document as an object. So, for example, if we wanted to get the URL of each file, the above example would reveal that we’re looking for the webViewLink. We can then pass that in as an argument:

{
  fields: 'files(name, webViewLink)',
}

While fields: '*' is useful for development, it’s better for performance to specify exactly which fields you want.

Accessing the Result

Let’s say we wanted to log our data in a local file on our machine. We could do this inside the callback function, and since this is a simple example, that would probably be fine.

But if you had a larger or more complex codebase, there are a lot of benefits to using async and await notation. Here’s one way of doing that:

(async function () {
  let res = await drive.files.list({
    pageSize: 5,
    fields: "files(name, webViewLink)",
    orderBy: "createdTime desc",
  });

  console.log(res.data);
})();

If you want to do this while retaining access to the err and result objects, we can use a Promise constructor, which allows us to resolve our result:

(async function () {
  let res = await new Promise((resolve, reject) => {
    drive.files.list(
      {
        pageSize: 5,
        fields: "files(name, webViewLink)",
        orderBy: "createdTime desc",
      },
      function (err, res) {
        if (err) {
          reject(err);
        }

        resolve(res);
      }
    );
  });

  console.log(res.data);
})();

Saving to a Local File

To store the data locally, one of the simplest methods is using the CSV format. Make sure to require Node’s filesystem module at the top of your file, using const fs = require('fs').

Then, after a bit of string manipulation, we can use fs.writeFile to create the file locally:

let data = "Name,URL\n";

res.data.files.map((entry) => {
  const { name, webViewLink } = entry;
  data += `${name},${webViewLink}\n`;
});

fs.writeFile("data.csv", data, (err) => {
  if (err) {
    throw err;
  }

  console.log("The file has been saved!");
});

To see the complete index.js file at this point, click here.

Transfering Data to Google Sheets

Saving a local file can be useful for debugging our code or backing up our data, but given we’re already using Google Drive, we’d probably prefer to log our results to Google Sheets.

To keep things clean, I prefer to import sheets immediately beneath where we imported drive:

const drive = google.drive({ version: "v3", auth });
const sheets = google.sheets({ version: "v4", auth });

We will:

  • create a new Google spreadsheet,
  • move it into a folder of our choice,
  • transfer ownership from the service account to our personal account,
  • insert the data as rows,
  • apply some simple formatting to the header row.

We’ll be awaiting the results of several functions, so make sure that all your code is within the same asynchronous function as above.

By demonstrating several methods, I hope you can become familiar with the general structure of Google’s API methods. There are, of course, many more methods than those used in this example, so make sure to check the reference for your own projects.

Create a New Spreadsheet

let newSheet = await sheets.spreadsheets.create({
  resource: {
    properties: {
      title: "A new day, a new sheet",
    },
  },
});

Move the Spreadsheet

Choose a folder of your choice and grab its ID from the URL. You can then pass it into the addParents property:

const updatedSheet = await drive.files.update({
  fileId: newSheet.data.spreadsheetId,
  addParents: directory,
  fields: "id, parents",
});

Transfer Ownership

By default, the service account will be the owner of any files it creates. While this might be fine in some circumstances, chances are we’ll want to access the file ourselves — at least for the purposes of debugging. Here’s how to transfer ownership.

await drive.permissions.create({
  fileId: newSheet.data.spreadsheetId,
  transferOwnership: "true",
  resource: {
    role: "owner",
    type: "user",
    emailAddress: "youremail@gmail.com",
  },
});

Add Data as New Rows

To add data, we’ll use the sheets.spreadsheets.values.append method. For now, we’ll populate our spreadsheet with the results of our list method above, but in practice, you can use whatever data you want!

let sheetData = [["File Name", "URL"]];

res.data.files.map((entry) => {
  const { name, webViewLink } = entry;
  sheetData.push([name, webViewLink]);
});

sheets.spreadsheets.values.append({
  spreadsheetId: newSheet.data.spreadsheetId,
  valueInputOption: "USER_ENTERED",
  range: "A1",
  resource: {
    range: "A1",
    majorDimension: "ROWS",
    values: sheetData,
  },
});

Add Styling to the First Row

The sheets.spreadsheets.batchUpdate method has a wide variety of possible properties, with nested objects sometimes going several layers deep. In this example, we’re making the cells of the first row black and the text of the first row white and bold.

await sheets.spreadsheets.batchUpdate({
  spreadsheetId: newSheet.data.spreadsheetId,
  resource: {
    requests: [
      {
        repeatCell: {
          range: {
            startRowIndex: 0,
            endRowIndex: 1,
          },
          cell: {
            userEnteredFormat: {
              backgroundColor: {
                red: 0.2,
                green: 0.2,
                blue: 0.2,
              },
              textFormat: {
                foregroundColor: {
                  red: 1,
                  green: 1,
                  blue: 1,
                },
                bold: true,
              },
            },
          },
          fields: "userEnteredFormat(backgroundColor,textFormat)",
        },
      },
    ],
  },
});

Bringing It All Together

Altogether, that’s over 100 lines of code, so here’s what our final index.js file should look like:

const { google } = require("googleapis");
const fs = require("fs");

const credentials = require("./credentials.json");

const scopes = ["https://www.googleapis.com/auth/drive"];

const auth = new google.auth.JWT(
  credentials.client_email,
  null,
  credentials.private_key,
  scopes
);

const drive = google.drive({ version: "v3", auth });
const sheets = google.sheets({ version: "v4", auth });

(async function () {
  let res = await drive.files.list({
    pageSize: 20,
    fields: "files(name,fullFileExtension,webViewLink)",
    orderBy: "createdTime desc",
  });

  // Create a new spreadsheet
  let newSheet = await sheets.spreadsheets.create({
    resource: {
      properties: {
        title: "Another Day, Another Spreadsheet",
      },
    },
  });

  // Move the spreadsheet
  const updatedSheet = await drive.files.update({
    fileId: newSheet.data.spreadsheetId,
    // Add your own file ID:
    addParents: "1Kyd0SwMUuDaIhs03XtKG849-d6Ku_hRE",
    fields: "id, parents",
  });

  // Transfer ownership
  await drive.permissions.create({
    fileId: newSheet.data.spreadsheetId,
    transferOwnership: "true",
    resource: {
      role: "owner",
      type: "user",
      // Add your own email address:
      emailAddress: "youremail@gmail.com",
    },
  });

  // Add data as new rows
  let sheetData = [["File Name", "URL"]];

  res.data.files.map((entry) => {
    const { name, webViewLink } = entry;
    sheetData.push([name, webViewLink]);
  });

  sheets.spreadsheets.values.append({
    spreadsheetId: newSheet.data.spreadsheetId,
    valueInputOption: "USER_ENTERED",
    range: "A1",
    resource: {
      range: "A1",
      majorDimension: "ROWS",
      values: sheetData,
    },
  });

  // Add styling to the first row
  await sheets.spreadsheets.batchUpdate({
    spreadsheetId: newSheet.data.spreadsheetId,
    resource: {
      requests: [
        {
          repeatCell: {
            range: {
              startRowIndex: 0,
              endRowIndex: 1,
            },
            cell: {
              userEnteredFormat: {
                backgroundColor: {
                  red: 0.2,
                  green: 0.2,
                  blue: 0.2,
                },
                textFormat: {
                  foregroundColor: {
                    red: 1,
                    green: 1,
                    blue: 1,
                  },
                  bold: true,
                },
              },
            },
            fields: "userEnteredFormat(backgroundColor,textFormat)",
          },
        },
      ],
    },
  });

  // Back-up data locally
  let data = "Name,URL\n";

  res.data.files.map((entry) => {
    const { name, webViewLink } = entry;
    data += `${name},${webViewLink}\n`;
  });

  fs.writeFile("data.csv", data, (err) => {
    if (err) throw err;
  });
})();

Overall, I hope this walkthrough has helped you feel more at home with Google’s APIs, and make you feel more comfortable consulting the reference for each API.

Of course, the data we’re using to populate a Google Doc or Sheet could be anything. One good use-case is sending scraper data straight to Google Sheets, and if you’d like to find out more about using Node.js for web scraping, check out my article on the topic.

© 2024 Bret Cameron