Google Sheets v4 API with Netlify Dev

Google Sheets’ API is very hard to use. I’ve tried and failed a few times. I finally figured it out today. Its worth it mainly because free usage is way cheaper than Airtable, but accordingly its docs aren’t really incentivized to help you figure out how to use them.

Anyway I figured it out. i’m tired so i cant write as nicely as I normally do but here are my notes.

Demo Here

For live demo check https://netlify-google-spreadsheet-demo.netlify.com/

googlesheets

How to get the env vars: an incomplete tutorial

These are my notes from when I ran it through the first time - in case you need it, here is another walkthrough with possibly better screenshots and instructions. You should be able to piece it together from here.

  1. make a google sheet. its id from url will be GOOGLE_SPREADSHEET_ID_FROM_URL

  2. head to google console and make sure the Sheets API is enabled https://console.developers.google.com/apis/library/sheets.googleapis.com?project=sixth-storm-268221. you may need to set up a “project” for this if this is your first time.

image

  1. get the service account key, NOT the API key. this might help: https://github.com/theoephraim/node-google-spreadsheet/blob/756d57fea3e1cf1d5ba6a38b12210102da0bf621/docs/getting-started/authentication.md. this will give you GOOGLE_SERVICE_ACCOUNT_EMAIL and GOOGLE_PRIVATE_KEY (see form below)

image

  1. Make sure your sheet has granted permission https://stackoverflow.com/questions/38949318/google-sheets-api-returns-the-caller-does-not-have-permission-when-using-serve to GOOGLE_SERVICE_ACCOUNT_EMAIL

to set this up for local dev

make sure to set env vars inside functions/google-spreadsheet-fn/.env:

TRY_TO=CUSTOMIZE_THIS
GOOGLE_SPREADSHEET_ID_FROM_URL= # e.g. 10abcu_reo5FctMpuiOYHJstj3lTit4pvp-VS7mZhgVw
GOOGLE_SERVICE_ACCOUNT_EMAIL= # e.g. googlenetlify-spreadsheet-test@foo-bar-123456.iam.gserviceaccount.com
GOOGLE_PRIVATE_KEY= # e.g. -----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG etc etc super long key

and then with the Netlify CLI you can run ntl dev (Netlify Dev) and it opens up locally for you to develop.

to set this up on your own in production on netlify

make sure to set the env vars in the netlify UI

Getting to CRUD

I have commented through the netlify function accordingly (source in github):

/*
 * prerequisites
 */
if (!process.env.NETLIFY) {
  // get local env vars if not in CI
  // if in CI i expect its already set via the Netlify UI
  require('dotenv').config();
}
// required env vars
if (!process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL)
  throw new Error('no GOOGLE_SERVICE_ACCOUNT_EMAIL env var set');
if (!process.env.GOOGLE_PRIVATE_KEY)
  throw new Error('no GOOGLE_PRIVATE_KEY env var set');
if (!process.env.GOOGLE_SPREADSHEET_ID_FROM_URL)
  // spreadsheet key is the long id in the sheets URL
  throw new Error('no GOOGLE_SPREADSHEET_ID_FROM_URL env var set');

/*
 * ok real work
 *
 * GET /.netlify/functions/google-spreadsheet-fn
 * GET /.netlify/functions/google-spreadsheet-fn/1
 * PUT /.netlify/functions/google-spreadsheet-fn/1
 * POST /.netlify/functions/google-spreadsheet-fn
 * DELETE /.netlify/functions/google-spreadsheet-fn/1
 *
 * the library also allows working just with cells,
 * but this example only shows CRUD on rows since thats more common
 */
const { GoogleSpreadsheet } = require('google-spreadsheet');

exports.handler = async (event, context) => {
  const UserIP = event.headers['x-nf-client-connection-ip'] || '6.9.6.9'; // not required, i just feel like using this info
  const doc = new GoogleSpreadsheet(process.env.GOOGLE_SPREADSHEET_ID_FROM_URL);

  // https://theoephraim.github.io/node-google-spreadsheet/#/getting-started/authentication
  await doc.useServiceAccountAuth({
    client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
    private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n')
  });
  await doc.loadInfo(); // loads document properties and worksheets. required.
  const sheet = doc.sheetsByIndex[0]; // you may want to customize this if you have more than 1 sheet
  // console.log('accessing', sheet.title, 'it has ', sheet.rowCount, ' rows');
  const path = event.path.replace(/\.netlify\/functions\/[^/]+/, '');
  const segments = path.split('/').filter((e) => e);

  try {
    switch (event.httpMethod) {
      case 'GET':
        /* GET /.netlify/functions/google-spreadsheet-fn */
        if (segments.length === 0) {
          const rows = await sheet.getRows(); // can pass in { limit, offset }
          const serializedRows = rows.map(serializeRow);
          return {
            statusCode: 200,
            // body: JSON.stringify(rows) // dont do this - has circular references
            body: JSON.stringify(serializedRows) // better
          };
        }
        /* GET /.netlify/functions/google-spreadsheet-fn/123456 */
        if (segments.length === 1) {
          const rowId = segments[0];
          const rows = await sheet.getRows(); // can pass in { limit, offset }
          const srow = serializeRow(rows[rowId]);
          return {
            statusCode: 200,
            body: JSON.stringify(srow) // just sends less data over the wire
          };
        } else {
          throw new Error(
            'too many segments in GET request - you should only call somehting like /.netlify/functions/google-spreadsheet-fn/123456 not /.netlify/functions/google-spreadsheet-fn/123456/789/101112'
          );
        }
      /* POST /.netlify/functions/google-spreadsheet-fn */
      case 'POST':
        /* parse the string body into a useable JS object */
        const data = JSON.parse(event.body);
        data.UserIP = UserIP;
        // console.log('`POST` invoked', data);
        const addedRow = await sheet.addRow(data);
        // console.log({ addedRow });
        return {
          statusCode: 200,
          body: JSON.stringify({
            message: `POST Success - added row ${addedRow._rowNumber - 1}`,
            rowNumber: addedRow._rowNumber - 1 // minus the header row
          })
        };
      /* PUT /.netlify/functions/google-spreadsheet-fn/123456 */
      case 'PUT':
        /* PUT /.netlify/functions/google-spreadsheet-fn */
        if (segments.length === 0) {
          console.error('PUT request must also have an id'); // we could allow mass-updating of the sheet, but nah
          return {
            statusCode: 422, // unprocessable entity https://stackoverflow.com/questions/3050518/what-http-status-response-code-should-i-use-if-the-request-is-missing-a-required
            body: 'PUT request must also have an id.'
          };
        }
        /* PUT /.netlify/functions/google-spreadsheet-fn/123456 */
        if (segments.length === 1) {
          const rowId = segments[0];
          const rows = await sheet.getRows(); // can pass in { limit, offset }
          const data = JSON.parse(event.body);
          data.UserIP = UserIP;
          console.log(`PUT invoked on row ${rowId}`, data);
          const selectedRow = rows[rowId];
          Object.entries(data).forEach(([k, v]) => {
            selectedRow[k] = v;
          });
          await selectedRow.save(); // save updates
          return {
            statusCode: 200,
            body: JSON.stringify({ message: 'PUT is a success!' })
            // body: JSON.stringify(rows[rowId]) // just sends less data over the wire
          };
        } else {
          return {
            statusCode: 500,
            body:
              'too many segments in PUT request - you should only call somehting like /.netlify/functions/google-spreadsheet-fn/123456 not /.netlify/functions/google-spreadsheet-fn/123456/789/101112'
          };
        }
      /* DELETE /.netlify/functions/google-spreadsheet-fn/123456 */
      case 'DELETE':
        //
        // warning:
        // this code is untested but you can probably figure this out
        //

        if (segments.length === 1) {
          const rows = await sheet.getRows(); // can pass in { limit, offset }
          // // we dont actually use this in the demo but you might
          // const rowId = segments[0];
          // await rows[rowId].delete(); // delete a row

          // do this
          if (rows.length > 1) {
            const lastRow = rows[rows.length - 1];
            await lastRow.delete(); // delete a row
            return {
              statusCode: 200,
              body: JSON.stringify({ message: 'DELETE is a success!' })
            };
          } else {
            return {
              statusCode: 200,
              body: JSON.stringify({
                message: 'no rows left to delete! (first row is sacred)'
              })
            };
          }
        } else {
          return {
            statusCode: 500,
            body: JSON.stringify({
              message:
                'invalid segments in DELETE request, must be /.netlify/functions/google-spreadsheet-fn/123456'
            })
          };
        }
      /* Fallthrough case */
      default:
        return {
          statusCode: 500,
          body: 'unrecognized HTTP Method, must be one of GET/POST/PUT/DELETE'
        };
    }
  } catch (err) {
    console.error('error ocurred in processing ', event);
    console.error(err);
    return {
      statusCode: 500,
      body: err.toString()
    };
  }

  /*
   * utils
   */
  function serializeRow(row) {
    let temp = {};
    sheet.headerValues.map((header) => {
      temp[header] = row[header];
    });
    return temp;
  }
};

Other resources

Tagged in: #tech #netlify #low code

Leave a reaction if you liked this post! 🧡
Loading comments...
Webmentions
❤️ 0 💬 29
  • dev-hoangweb24.pantheonsite.io  admin mentioned this on 2022-04-21

    Hits: 2

    Trong vài năm qua, các chức năng không máy chủ (đôi khi còn được gọi là “máy chủ không máy chủ” hoặc “máy tính không máy chủ”) đã trở thành một công nghệ phổ biến. Tuy nhiên, vẫn còn nhiều sự nhầm lẫn xung quanh thuật ngữ này. Là

  • hashtagbeginner.com  mentioned this on 2022-03-28
  • unnecessarytips.com  mentioned this on 2021-07-28
  • www.pixellyft.com  admin mentioned this on 2021-07-21


    Over the last few years, serverless functions (sometimes also referred to as “serverless” or “serverless computing”) have become a popular technology. However, there’s still a lot of confusion around the term. How can you run code without servers? What are the pros and con

  • www.rholwebs.com  mentioned this on 2021-07-05
  • digitasbuzz.in  mentioned this on 2021-07-02
  • avatar of
    mentioned this on 2021-06-11

    Skip to main content

    Free JavaScript Book!

  • mediastream.wpdesigns.live  mentioned this on 2021-05-15
  • techsponent.tech  mentioned this on 2021-05-13
  • wptutos.weboptime.com  mentioned this on 2021-04-17
  • cloudrelics.com  mentioned this on 2021-04-13

    Over the last few years, serverless functions (sometimes also referred to as “serverless” or “serverless computing”) have become a popular technology. However, there’s still a lot of confusion around the term. How can you run code without servers? What are the pros and cons of th

  • prema.kapilaya.net  mentioned this on 2021-03-30
  • www.skiewebs.com  mentioned this on 2021-03-16
  • click2down.com  mentioned this on 2021-03-16
  • docuneedsph.com  mentioned this on 2021-03-16
  • gsensenews.com  mentioned this on 2021-03-16
  • avatar of リッフィー レー
    リッフィー レー retweeted
  • avatar of Thiago Magalhães 😎
    Thiago Magalhães 😎 retweeted
  • avatar of -1
    -1 retweeted
  • avatar of Ahmad Ismail
    Ahmad Ismail retweeted
  • avatar of Wiput Pootong
    Wiput Pootong retweeted
  • doodde.ru  admin mentioned this on 2021-03-16


    Over the last few years, serverless functions (sometimes also referred to as “serverless” or “serverless computing”) has become a popular technology. However, there’s still a lot of confusion around the term. How can you run code without servers? What are the pros and cons

  • 711web.com  mentioned this on 2021-03-16
  • editor.sitepoint.com  mentioned this on 2021-03-16
  • avatar of Jacob M-G Evans ⚛
    Jacob M-G Evans ⚛ retweeted
  • avatar of Paul Scanlon 👻
    Paul Scanlon 👻 retweeted
  • avatar of adokce
    adokce retweeted
  • avatar of Boseok In
    Boseok In retweeted
  • avatar of shimon
    shimon retweeted
  • avatar of おおやま確定申告みちのく
    おおやま確定申告みちのく retweeted
  • avatar of 全部入りHTML太郎
    全部入りHTML太郎 retweeted
  • avatar of nacano
    nacano retweeted
  • avatar of A-J Roos
    A-J Roos retweeted
  • avatar of maxzz
    maxzz retweeted

Subscribe to the newsletter

Join >10,000 subscribers getting occasional updates on new posts and projects!

I also write an AI newsletter and a DevRel/DevTools newsletter.

Latest Posts

Search and see all content