Using Google Sheets as a RESTful JSON API

In this article, we'll cover how to create a simple and free RESTful JSON API, using Google Sheets as a data source. This can be a very effective pattern for simple CRUD applications - anywhere the data is managed by non-technical users, with an app which is a simple consumer of that data.

For our example, we'll work with election results, showing the seats won by each party. The data can be updated in a google sheet by an editorial team, and will be converted into an API endpoint for a frontend chart or table to feed from. This type of niche and specific data can be a pain to shoehorn into existing systems, so is a perfect candidate for our lightweight approach here!

Advantages of using Google Sheets as an API

Google Sheets is a powerful tool for managing data, and is widely used by non-technical users. It provides a familiar interface for managing data, and can be easily shared and collaborated on. Things like user authentication are already built into Google Sheets, and it can be accessed from anywhere with an internet connection. This makes it a great choice for managing data for simple CRUD applications.

Setting up the Google Sheet

The first step is to create a new Google Sheet, and add some data to it. For our election example, we'll create a simple sheet with a list of political parties, and the seats they have won so far.

PartySeats
Conservative Progressives24
Democratic Socialist Coalition18
Independents10
Judean People's Front16
People's Front of Judea17
Progressive Conservative Party25
Social Democratic Alliance19

Next we need to write the script which will convert this data into a JSON endpoint. In Google Sheets, go to "Extensions" then "Apps Script".

Open Apps Script
Open Apps Script

We'll name the project "Election Tracker", then clear the code stub from the code window.

Rename project, and clear out code editor
Rename project, and clear out code editor

Google Apps Script reserves the doGet and doPost function names to handle GET and POST requests. We're creating a simple GET endpoint, and aren't implementing any kind of auth on it - it's a public endpoint. However, Google's account access and auth can be a bit fiddly, so we'll start with a simple API response and focus on getting the auth out of the way first. Let's start by putting this placeholder code into the Apps Script window:

function doGet() {
  let result = {"message":"hello"};
  return ContentService.createTextOutput(JSON.stringify(result))
    .setMimeType(ContentService.MimeType.JSON);
}

Publishing the Google Sheet

Next, we need to publish the Google Sheet to the web. This makes the data accessible via a URL, which we can use to access the data via the API. Click the blue "Deploy" button in the top right of the screen, then on the dropdown "New deployment". From the window which appears, click the cog beside "Select Type" and select "Web app".

New deployment - web app configuration
New deployment - web app configuration

Give our deployment a description. Every time we do a new deployment, we'll be asked to enter a new description, so think of this like a commit log. "Initial Test", "Seat Count" or the like is fine for the first pass here. Now we set up access permissions. If we want to allow general public access to the API, the "Execute as" setting should be your google account, and in "Who has access", set it to "Anyone". Then click "Deploy".

Deployment configuration
Deployment configuration

The deployment modal will give a deployment id, and a url. Checking this URL will redirect us to a specific deployment URL, and take a few seconds to load, but it should ultimately show the test payload we were expecting.

Successful deployment
Successful deployment
{
  "message": "hello"
}

Calling this URL via curl or in an incognito window confirms that it's working ok for public access, i.e. we're not sneaking through thanks to our logged-in google account. Now that we've got the flow working, it's time to implement the full script!

Implementing the API

What we want to do is:

  • Load the main data sheet from the script.
  • Read in all party + seat data.
  • Sort descending.
  • Output as json.

Firstly, ensure that the sheet has been named "Seats". Then update the code in the Apps Script to:

// This name must match our sheet name
const SHEET_NAME = "Seats";

// GET Function: Retrieve seat counts
function doGet() {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
    if (!sheet) {
        throw new Error("Sheet not found");
    }
    const data = sheet.getDataRange().getValues();

    const seatCounts = {};
    // Start from 1 to skip the header row
    for (let i = 1; i < data.length; i++) {
        // Assuming data is in PARTY,COUNT format
        // @TODO: Toy example here, but error checking would
        // be required in a more serious implementation
        seatCounts[data[i][0]] = data[i][1];
    }

    // Convert to ordered array
    const result = Object.entries(seatCounts)
        .map(([party, total]) => ({ party, total }))
        .sort((a, b) => b.total - a.total);

    return ContentService.createTextOutput(JSON.stringify(result))
        .setMimeType(ContentService.MimeType.JSON);

  } catch (error) {

    return ContentService.createTextOutput(
        JSON.stringify({ error: error.message })
    ).setMimeType(ContentService.MimeType.JSON);

  }
}

Click "Deploy -> New Deployment" and enter a new description. Now that we're linking our script to sheet data, we're asked for additional permissions on our google account. Grant these via the oauth window which pops up, and we get another deployment url. Notice that this one is different to the one we got earlier - each deployment gets its own unique URL.

Last auth step!
Last auth step!

Accessing this URL again loads our sorted json:

[
  {
    "party": "Progressive Conservative Party",
    "total": 25
  },
  {
    "party": "Conservative Progressives",
    "total": 24
  },
  {
    "party": "Social Democratic Alliance",
    "total": 19
  },
  {
    "party": "Democratic Socialist Coalition",
    "total": 18
  },
  {
    "party": "People's Front of Judea",
    "total": 17
  },
  {
    "party": "Judean People's Front",
    "total": 16
  },
  {
    "party": "Independents",
    "total": 10
  }
]

Accessing the Google Sheets API

As noted above, the URL Google Sheets gives us will 302 to another, longer URL. When we're working with the URL from a frontend, make sure to use a library which will follow 302s. Alternatively you could click the URL, see where it's 302d to, and use that URL in your application, but it's likely safer to go with the URL Google give, as it's no guarantee the 302 won't change to point at a different URL at some point.

For example, javascript's fetch will automatically handle a 302 fine:

const url = 'https://script.google.com/macros/s/SCRIPT_URL_HERE/exec';
fetch(url)
  .then(response => response.json())
  .then(data => console.log(data));
Loading from the API
Loading from the API

Once this info is accessible, it can be used to build whatever type of visualisation is required at app level:

Visualising the results
Visualising the results

Updating the Google Sheet

One of the great things about this setup is that non-technical users can easily update the data in the Google Sheet, and the changes will be reflected in the API response within a matter of seconds. This makes it easy to manage data for simple CRUD applications without needing to build a dedicated backend.

Rate limits

One thing to keep in mind when using the Sheets API is that Google does apply rate limits:

Google Apps Script rate limits
Google Apps Script rate limits

If the app is going to be in heavy usage, it may be worth exploring caching of the response to avoid rate limits. I've written in a bit more detail about a nice serverless approach to managing this cache using Cloudlare workers.

Conclusion

Using Google Sheets as a data source for a simple RESTful JSON API can be a great way to quickly build simple CRUD applications. It allows non-technical users to manage the data directly, and provides a familiar interface for doing so. This can be a very effective pattern for simple applications or prototypes, where the data is the primary focus, and the app is a simple consumer of that data. The API itself can be a little slow to load (2-3 seconds load time isn't unusual), but as a way to stand up a quick backend, it's definitely an option worth exploring. And when coupled with serverless API caching via Cloudflare Workers, we can quickly get the guts of a powerful and reliable backend API in place!


CyberWiseCon 2025 Speaker

CyberWiseCon 2025

In May 2025, I'll be giving a talk at CyberWiseCon 2025 in Vilnius, Lithuania. From selling 10 Downing St, to moving the Eiffel Tower to Dublin, this talk covers real-world examples of unconventional ways to stop scrapers, phishers, and content thieves. You'll gain practical insights to protect assets, outsmart bad actors, and avoid the mistakes we made along the way!

Get your ticket now and I'll see you there!


Share This Article

Related Articles


Lazy loading background images to improve load time performance

Lazy loading of images helps to radically speed up initial page load. Rich site designs often call for background images, which can't be lazily loaded in the same way. How can we keep our designs, while optimising for a fast initial load?

Serverless caching and proxying with Cloudflare Workers

Using Cloudflare Workers we can quickly build an effective API proxy, without spinning up any additional hardware. Whether its needing a CORS proxy, speeding up slow APIs via caching, or rate limit management on stingy APIs, this serverless tech is as easy to set up as it is powerful.

Idempotency - what is it, and how can it help our Laravel APIs?

Idempotency is a critical concept to be aware of when building robust APIs, and is baked into the SDKs of companies like Stripe, Paypal, Shopify, and Amazon. But what exactly is idempotency? And how can we easily add support for it to our Laravel APIs?

Calculating rolling averages with Laravel Collections

Rolling averages are perfect for smoothing out time-series data, helping you to gain insight from noisy graphs and tables. This new package adds first-class support to Laravel Collections for rolling average calculation.

More