All posts
Blog

CRUD Operations on Google Sheets via REST API

How to create, read, update, and delete rows in Google Sheets using GKit SheetsAPI — with real fetch examples in TypeScript.

3 min read

Google Sheets as a CRUD database

Most REST APIs talk to Postgres or MongoDB. GKit SheetsAPI talks to a Google Sheet — and it supports the full set of CRUD operations: create, read, update, and delete rows. If your data already lives in a spreadsheet, you get a real REST API without writing a single line of backend code.

This guide walks through every operation with working TypeScript fetch examples.

Setup: your base URL and auth header

Every request goes to:

https://sheetsapi.gkit.mreshank.com/api/spreadsheets/{userKey}/{sheetName}
  • {userKey} — found in your GKit dashboard after connecting a spreadsheet
  • {sheetName} — the tab name inside your Google Sheet (e.g. Contacts, Products)

All mutating requests (POST, PUT, DELETE) require an API key:

Authorization: Bearer sk_...

Create one in the dashboard under API Keys. Read-only GET requests work without auth on public sheets.

Read all rows

const BASE = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets";
 
async function listRows(userKey: string, sheet: string) {
  const res = await fetch(`${BASE}/${userKey}/${sheet}`, {
    headers: { Authorization: `Bearer ${process.env.GKIT_API_KEY}` },
  });
  const json = await res.json();
  // { data: [...], meta: { total: 42, limit: 100, offset: 0 } }
  return json;
}

The response always includes a meta object so you can implement pagination:

async function listPage(userKey: string, sheet: string, offset = 0) {
  const url = new URL(`${BASE}/${userKey}/${sheet}`);
  url.searchParams.set("limit", "20");
  url.searchParams.set("offset", String(offset));
 
  const res = await fetch(url.toString(), {
    headers: { Authorization: `Bearer ${process.env.GKIT_API_KEY}` },
  });
  return res.json(); // { data: [...], meta: { total, limit, offset } }
}

Read a single row

Rows are addressed by their 1-based index — row 1 is the first data row (the header row is row 0 and is never returned as data).

async function getRow(userKey: string, sheet: string, row: number) {
  const res = await fetch(`${BASE}/${userKey}/${sheet}/${row}`, {
    headers: { Authorization: `Bearer ${process.env.GKIT_API_KEY}` },
  });
  if (!res.ok) throw new Error(`Row ${row} not found`);
  return res.json(); // { name: "Ada", email: "ada@example.com", plan: "pro" }
}

Create a row

POST a JSON object whose keys match the column headers in your sheet. GKit appends a new row at the end.

async function createRow(
  userKey: string,
  sheet: string,
  data: Record<string, string>
) {
  const res = await fetch(`${BASE}/${userKey}/${sheet}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.GKIT_API_KEY}`,
    },
    body: JSON.stringify(data),
  });
  return res.json();
}
 
// Example
await createRow("uk_abc123", "Contacts", {
  name: "Grace Hopper",
  email: "grace@example.com",
  plan: "free",
});

Any keys that don't match a header are silently ignored. Missing keys leave the cell empty.

Update a row

PUT replaces the row at the given index. Supply only the fields you want to change — unchanged columns keep their current values.

async function updateRow(
  userKey: string,
  sheet: string,
  row: number,
  data: Record<string, string>
) {
  const res = await fetch(`${BASE}/${userKey}/${sheet}/${row}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.GKIT_API_KEY}`,
    },
    body: JSON.stringify(data),
  });
  return res.json();
}
 
// Upgrade row 3 to "pro"
await updateRow("uk_abc123", "Contacts", 3, { plan: "pro" });

Delete a row

DELETE removes the row entirely. Rows below it shift up, so subsequent operations should re-fetch the index rather than cache it.

async function deleteRow(userKey: string, sheet: string, row: number) {
  const res = await fetch(`${BASE}/${userKey}/${sheet}/${row}`, {
    method: "DELETE",
    headers: { Authorization: `Bearer ${process.env.GKIT_API_KEY}` },
  });
  if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
  return res.status === 204; // true on success
}
 
await deleteRow("uk_abc123", "Contacts", 3);

Putting it together: a typed client

Wrap the four operations in a small utility and you have a fully typed spreadsheet client in under 60 lines:

const BASE = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets";
 
export class SheetsClient<T extends Record<string, string>> {
  constructor(
    private userKey: string,
    private sheet: string,
    private apiKey: string
  ) {}
 
  private headers() {
    return {
      "Content-Type": "application/json",
      Authorization: `Bearer ${this.apiKey}`,
    };
  }
 
  async list(): Promise<{ data: T[]; meta: { total: number; limit: number; offset: number } }> {
    return fetch(`${BASE}/${this.userKey}/${this.sheet}`, {
      headers: this.headers(),
    }).then((r) => r.json());
  }
 
  async get(row: number): Promise<T> {
    return fetch(`${BASE}/${this.userKey}/${this.sheet}/${row}`, {
      headers: this.headers(),
    }).then((r) => r.json());
  }
 
  async create(data: Partial<T>): Promise<T> {
    return fetch(`${BASE}/${this.userKey}/${this.sheet}`, {
      method: "POST",
      headers: this.headers(),
      body: JSON.stringify(data),
    }).then((r) => r.json());
  }
 
  async update(row: number, data: Partial<T>): Promise<T> {
    return fetch(`${BASE}/${this.userKey}/${this.sheet}/${row}`, {
      method: "PUT",
      headers: this.headers(),
      body: JSON.stringify(data),
    }).then((r) => r.json());
  }
 
  async delete(row: number): Promise<boolean> {
    const r = await fetch(`${BASE}/${this.userKey}/${this.sheet}/${row}`, {
      method: "DELETE",
      headers: this.headers(),
    });
    return r.status === 204;
  }
}

What's next

Share