Accessing data in React (static) app using API

I’m building a simple web app, hoping to use Grist as a backend and a React app hosted on github pages as the frontend. (The project has a $0 budget and is being developed by students as a free public educational resource.)

I’ve managed to do this successfully with Airtables, but when I try doing the exact same with Grist, I always get access problems. I’m guessing the issue is with my fetch() call, but try as I might, I can’t get anything to work. curl works just fine, and they should be identical, but no luck.

Can you provide a sample API call with required header info?

After working on it some more, I discovered that I don’t actually need an API key to access the data if I make it public. However, I still have problems with accessing it from JS. Now I get:

[Error] Origin http://localhost:3000 is not allowed by Access-Control-Allow-Origin. Status code: 500

The cause seems like something to do with SOP now, but I still don’t understand how to fix it. Is there something I can do on my end to make this work? Or does Grist simply not allow access from localhost?

Hi @Hieroglyphs_in_Real.

Grist limits API usage to only non-browser environments (like node.js), because authentication via API key is generally not safe to use in a browser. That is why, the curl request works without any problem. You can read more about it in Cross-Origin Resource Sharing (CORS) - HTTP | MDN

We plan to relax those restrictions in the future. But for now, the only option is to move your API logic to some back-end solution that you trust, which will act as a middle man between your page and Grist API. You can use services like: netlify, vercel, or napkin.io to host your service-side code. But please remember, this is not a very secure solution.

For example, for napkin.io you can deploy a simple “serverless function” called api, that can be used from a browser, with the following code:

const fetch = require('node-fetch'); // You need to add module node-fetch
export default async (req, res) => {
  const url = 'https://docs.getgrist.com' + req.path;
  const result = await fetch(url, {
    headers: {
      "Authorization": req.get('Authorization'),
      'Content-Type': 'application/json'
    },
    method: req.method,
    body: JSON.stringify(req.body)
  })
  res.set(result.status).json(await result.json());
}

I think, it should be good enough to route any requests to the Grist API https://docs.getgrist.com/api/ through your serveless function (i.e. https://<your_account_name>.npkn.net/api/). So in all your API calls, you just need to replace “https://docs.getgrist.com” with the URL of your function.

Hi Jarek,

Thank you so much for your help. Aside from the added layer of napkin.io, which is pretty easy to manage as these things go, this is a perfect solution. I’m also very grateful for the sample code. It’s always extra frustrating to troubleshoot when you can’t get anything working to begin with, but having this example let me tinker around and get exactly what I needed.

If I can impose on you a bit more, I would like to ask about security, because this is not a subject I know much about. The way I have things set up now, I made a dummy user account and connected it to the table with “viewer” access. Theoretically a savvy person could dig into the JS, pull out the key, and use it to access the data. I’m fine with that because I intend to create a sort of “SQL dump” type endpoint to enable anyone to download a snapshot of the data for their own use. I’m even considering making the data all publicly accessible for viewing anyway. This is an open-data project after all. I would only worry about a malicious actor finding a way to modify the data. Are there any other security concerns I need to worry about, assuming that I’m fine with people looking at the data whenever they want?

On a slightly different subject, you mentioned relaxing these restrictions in the future. I think this project might be an ideal use case for that, if this is something Grist is looking for. It’s an educational website for the public designed to be sustainable in the long term by using only free resources. There is no need to restrict access to viewing the data. The work is entirely pro bono publico. I know of at least a dozen academic projects that need this sort of functionality: easy to use data services with backends that any student can deal with regardless of technical experience (e.g. the Grist UI), plus user-friendly frontends that don’t require paying a monthly fee for hosting. The current academic funding situation only pays for development, so it doesn’t allow for data-driven websites that stay online indefinitely. I’m giving a talk about this exact problem at an academic conference in November. Perhaps there’s a valuable promotional opportunity for Grist in there? Something along the lines of: “Grist powers linked open data academic projects that make knowledge freely available to everyone” (a button like that in the footer of academic websites might do a lot of good). I already plan to speak highly of your service, but perhaps you (or others at Grist) would like to discuss this further? If so, feel free to contact me about it.

Thanks again,
Christian

I don’t understand why this is not secure. If the API key is on the server side (which it should always be - there really isn’t a need for client side keys), and not hard-written into the code (i.e. use ENV variables) it is never exposed to anyone and there is nothing to worry about. No different than the risk of Grist itself storing the API key.

BTW, I have built a small website for a party with Grist. I use NextJs and you can call Grist backend via the grist Api (import {GristDocAPI} from ‘grist-api’). You do this in the getStaticPaths and getStaticProps in NextJs. As these are only executed server side there is no risk whatsoever and the API will work fine. you will have to just review the API documentation. Actually, it’s quite simple and I’m not sure why Grist doesn’t push this use case more. Grist is a perfect solution for small static websites, which is basically 90% of the Internet. Built the whole thing in about an hour (granted I had frontend components already). Below is the basic code snippet for NextJs (disclaimer: this is a quick cut and paste, but it should get you started):

import {GristDocAPI} from 'grist-api'
export async function getStaticPaths() {
  
    const DOC_URL = "{YOURGRISTDOCURL}";
    const API_KEY = process.env.YOURGRISTAPIKEY
    const api = new GristDocAPI(DOC_URL, {apiKey: API_KEY} );
    const table = "Posts"
    const data = await api.fetchTable(table);  //must have a slug column that must be unique, use UUID() to make this easy
  
    return {
      paths:
        data?.map(({ slug }) => {
          return {
            params: {
              slug,
            },
          };
        }) || [],
      fallback: 'blocking',
    };
  }


  export async function getStaticProps({ params }) {
    const DOC_URL = "{YOURGRISTDOCURL}";
    const API_KEY = process.env.YOURGRISTAPIKEY
    const api = new GristDocAPI(DOC_URL, {apiKey: API_KEY} );
    const table = "Posts"
    const filters = {slug: [`${params.slug}`]}
    const data = await api.fetchTable(table,filters);
    const post = data
    if (!post) {
      return {
        notFound: true,
      };
    }
  
    return {
      props: { post},
      revalidate: 60, // In seconds
    };
  }

  export default function Blog({
    post
  }) {
    console.log ("post", post)
//do what you want with the post
   
    return (
      <>
        
      </>
    );
  }

Hi Christian,

Your solution looks good, another account in a Viewer role won’t allow anyone to modify your main document data (but still will allow modifying documents that belong to that account).

To answer @ddsgadget question, the generic solution I proposed is still using a key that is stored on the client (and is passed in the Authorize header). You can, of course, move this key to a server-side environmental variable (I think napkin.io supports it), but I think this would be even worse. Since the function code is very generic, anyone would be able to execute it without any kind of authentication (since the API key is hard-coded on the server). To make it better, you would need to implement some kind of authentication layer on top of it, that would effectively “hide” the API key from unauthenticated users.

Of course, the “best” solution is the one that @ddsgadget suggested. To have a dedicated API, that is hidden by some authentication layer, which uses Grist as an external database.

Hi Jarek,

Glad to hear it.

I have a followup question, which I’m happy to create a new thread around if you think it’s interesting enough. I’ve got everything working in my API calls, except for the small issue of Napkin’s transfer size limit. This means that, for example, I can’t just ask for an entire table with more than 500 rows. My solution to this is to put the main indexing data in a single table, put the additional info in another table (of identical length). Then I can request the much smaller index table and use the ids from that plus the filter parameter to get data from a bigger table.

It occurs to me that being able to request specific fields would eliminate the need for two separate tables. As it is now, API calls are all like: SELECT * FROM table WHERE filter. A SELECT field_name FROM table [WHERE filter] would also be really useful. I read all the docs and didn’t find this, but please feel free to tell me if it exists and I just missed it somehow. If it does not exist, can I make it a feature request?

Thanks again for all your help so far.

Christian

Hi Christian,

Currently, no endpoint can return only a subset of all available columns. I’m sorry you are hitting some data limits, but I’m glad you have found some workaround.

Yes, feel free to post this feature request on Grist Feedback - Grist Creators. This would be very useful for other use cases that are similar to your one.

Thanks,
Jarek

Thanks Jarek. I will do exactly that. It’s certainly not urgent, but perhaps it could become a valued feature without overburdening your team with too complex a request. Worth a mention anyway.

Thanks again,
Christian