Apps Script example · 11 min read

CRUD Web App Sheets: Copy-Paste Apps Script Pattern

Working crud web app sheets example in Apps Script—copy-paste code, common mistakes, and when to get it built professionally.

HtmlServiceCRUDSheets

Many internal tools need full CRUD against a sheet without exposing the spreadsheet UI.

listContacts / createContact / updateContact / deleteContact form a small API over columns A–E on Contacts.

IDs are UUIDs so deletes and updates do not depend on volatile row numbers from the client.

Pair with an HTML table UI that calls these via google.script.run and re-lists after each mutation.

Need this built? Hire a Google Apps Script developer →

Sheet / project setup

SheetColumnPurpose
ContactsA idUUID primary key
ContactsB nameDisplay name
ContactsC emailEmail
ContactsD phonePhone optional
ContactsE updatedAtLast write time

What this script does

The four functions cover read and write paths; findRowById_ translates UUID to a 1-based row index.

Prerequisites

Contacts sheet with headers; web app or sidebar that calls the functions.

  • Header row present
  • Clients never send row numbers
  • Authn/authz added for production

Walkthrough

createContact then listContacts; update fields; deleteContact; confirm the row is gone.

Edge cases

Concurrent edits can race — use LockService around update/delete in busy teams.

  • deleteRow shifts indexes — always re-find by id
  • Empty id rows are skipped in list

How to test

Create two contacts, update one phone, delete the other, list and assert length 1.

Hardening for production

Validate email format; soft-delete with a status column instead of deleteRow for audit.

Variations

Add pagination with start/limit; or move storage to Workspace database / JDBC.

Full code: Contacts CRUD

Call listContacts/createContact/updateContact/deleteContact from HtmlService. Keep UUIDs as the client identity.

/**
 * Minimal CRUD API for a Contacts sheet, called from HtmlService UI.
 * Columns: A id, B name, C email, D phone, E updatedAt
 */
const CONTACT_SHEET = "Contacts";

function listContacts() {
  const sheet = sh_();
  const values = sheet.getDataRange().getValues();
  const out = [];
  for (let i = 1; i < values.length; i++) {
    if (!values[i][0]) continue;
    out.push({
      id: values[i][0],
      name: values[i][1],
      email: values[i][2],
      phone: values[i][3],
      updatedAt: values[i][4],
    });
  }
  return out;
}

function createContact(data) {
  const id = Utilities.getUuid();
  sh_().appendRow([id, data.name, data.email, data.phone || "", new Date()]);
  return { id: id };
}

function updateContact(id, data) {
  const row = findRowById_(id);
  if (!row) throw new Error("Not found");
  const sheet = sh_();
  sheet.getRange(row, 2, row, 5).setValues([[data.name, data.email, data.phone || "", new Date()]]);
  return { ok: true };
}

function deleteContact(id) {
  const row = findRowById_(id);
  if (!row) throw new Error("Not found");
  sh_().deleteRow(row);
  return { ok: true };
}

function sh_() {
  return SpreadsheetApp.getActive().getSheetByName(CONTACT_SHEET);
}

function findRowById_(id) {
  const values = sh_().getDataRange().getValues();
  for (let i = 1; i < values.length; i++) {
    if (values[i][0] === id) return i + 1;
  }
  return null;
}
  1. Line 7: Returns plain objects for JSON-friendly script.run.
  2. Line 25: Creates a stable primary key for each contact.
  3. Line 26: Inserts a new contact at the bottom.
  4. Line 31: Maps UUID to sheet row number.
  5. Line 34: Updates name/email/phone/updatedAt in place.
  6. Line 41: Removes the contact row after id lookup.

Deploy this example

  1. 01

    Open Apps Script

    In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.

  2. 02

    Paste and save

    Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.

  3. 03

    Authorize once

    Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.

  4. 04

    Add the trigger

    Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.

Before you run: sheet CRUD

  • 1Contacts headers: id, name, email, phone, updatedAt
  • 2UI uses returned id for update/delete
  • 3Add domain or role checks for write methods
  • 4Test concurrent updates if multiple clerks edit
  • 5Backup the sheet before mass deletes
  • 6Decide soft-delete vs hard delete

Frequently asked questions

Row numbers change after inserts/deletes. UUIDs stay stable.

Accept offset/limit parameters and slice the out array, or filter server-side.

Yes without locks. Use LockService.tryLock around updateContact.

Add listContactsByEmail or filter in listContacts before return.

Fine for small internal tools. Move to a real DB when you need transactions or large scale.

doGet returns HtmlService output; client JS calls these function names via google.script.run.

Accept an array and setValues a block after building rows — faster than many appendRow calls.

Related examples

Want this wired into your real workflow?

I adapt these patterns to your Sheet structure, APIs, and triggers — deployed in your Google account. Fixed-scope quotes from $500 · free 30-min consult · quote within 24 hours.