Plantilla base para construir una web app de Google Apps Script usando Vite y publicar el resultado con clasp.
- Instala dependencias con
pnpm install. - Copia
.clasp.json.examplea.clasp.json. - Reemplaza
scriptIdcon el ID de tu proyecto de Apps Script. - Inicia sesión si hace falta con
npx clasp login. - Ejecuta
pnpm devpara desarrollo local con Vite. - Ejecuta
pnpm pushpara compilar y subir a Apps Script.
pnpm dev: servidor local de Vite.pnpm build: generadist/compatible con Apps Script.pnpm push: compila y sube a Apps Script.pnpm deploy: compila, sube y crea un deployment.
Una forma muy común de trabajar con Google Apps Script es usar un spreadsheet como si fuera una base de datos simple.
- Un spreadsheet representa la base de datos.
- Cada hoja representa una tabla.
- La primera fila de cada hoja contiene los nombres de las columnas.
- Cada fila siguiente representa un registro.
- Conviene tener una columna
idcomo identificador único.
Ejemplo de hoja users:
| id | name | createdAt | |
|---|---|---|---|
usr_001 | Ana | ana@email.com | 2026-05-08T10:00:00.000Z |
usr_002 | Luis | luis@email.com | 2026-05-08T10:30:00.000Z |
La idea es centralizar el acceso a Sheets en funciones de servidor dentro de Apps Script.
Puedes empezar con algo como esto en Code.js o moverlo luego a otro archivo .gs.
constDB_CONFIG={spreadsheetId: 'REEMPLAZA_CON_TU_SPREADSHEET_ID',};functiongetDb(){returnSpreadsheetApp.openById(DB_CONFIG.spreadsheetId);}functiongetTable(tableName){constsheet=getDb().getSheetByName(tableName);if(!sheet){thrownewError(`La hoja "${tableName}" no existe.`);}returnsheet;}functiongetHeaders(sheet){constlastColumn=sheet.getLastColumn();if(lastColumn===0){return[];}returnsheet.getRange(1,1,1,lastColumn).getValues()[0];}functiongetRows(sheet){constlastRow=sheet.getLastRow();constlastColumn=sheet.getLastColumn();if(lastRow<2||lastColumn===0){return[];}returnsheet.getRange(2,1,lastRow-1,lastColumn).getValues();}functionmapRow(headers,row){returnheaders.reduce((record,header,index)=>{record[header]=row[index];returnrecord;},{});}functionmapRows(headers,rows){returnrows.map((row)=>mapRow(headers,row));}Inserta un nuevo registro al final de la hoja.
functioninsertRecord(tableName,payload){constsheet=getTable(tableName);constheaders=getHeaders(sheet);if(!headers.length){thrownewError(`La hoja "${tableName}" no tiene encabezados.`);}constrecord={id: payload.id||Utilities.getUuid(),createdAt: payload.createdAt||newDate().toISOString(),
...payload,};constrow=headers.map((header)=>{returnheaderinrecord ? record[header] : '';});sheet.appendRow(row);returnrecord;}Ejemplo:
functioncreateUser(){returninsertRecord('users',{name: 'Ana',email: 'ana@email.com',});}Obtiene todos los registros de una hoja.
functiongetAllRecords(tableName){constsheet=getTable(tableName);constheaders=getHeaders(sheet);constrows=getRows(sheet);returnmapRows(headers,rows);}Ejemplo:
functionlistUsers(){returngetAllRecords('users');}Busca un registro por su id.
functiongetRecordById(tableName,id){constrecords=getAllRecords(tableName);returnrecords.find((record)=>String(record.id)===String(id))||null;}Ejemplo:
functionfindUser(){returngetRecordById('users','usr_001');}Actualiza una fila existente buscando por id.
functionupdateRecord(tableName,id,payload){constsheet=getTable(tableName);constheaders=getHeaders(sheet);constrows=getRows(sheet);constrowIndex=rows.findIndex((row)=>String(row[0])===String(id));if(rowIndex===-1){thrownewError(`No se encontró un registro con id "${id}".`);}constcurrentRecord=mapRow(headers,rows[rowIndex]);constnextRecord={
...currentRecord,
...payload,id: currentRecord.id,};constnextRow=headers.map((header)=>{returnheaderinnextRecord ? nextRecord[header] : '';});sheet.getRange(rowIndex+2,1,1,headers.length).setValues([nextRow]);returnnextRecord;}Ejemplo:
functioneditUser(){returnupdateRecord('users','usr_001',{name: 'Ana Maria',});}Elimina una fila por id.
functiondeleteRecord(tableName,id){constsheet=getTable(tableName);constrows=getRows(sheet);constrowIndex=rows.findIndex((row)=>String(row[0])===String(id));if(rowIndex===-1){thrownewError(`No se encontró un registro con id "${id}".`);}sheet.deleteRow(rowIndex+2);return{success: true, id };}Ejemplo:
functionremoveUser(){returndeleteRecord('users','usr_001');}Para que el CRUD sea fácil de mantener, intenta seguir estas reglas:
- Usa siempre la fila 1 como encabezado.
- Deja
iden la primera columna. - Usa nombres de columnas estables:
name,email,status,createdAt,updatedAt. - Evita celdas combinadas, colores como fuente de datos o fórmulas mezcladas con registros.
Cuando la app ya corre dentro de Google Apps Script, puedes usar google.script.run.
Ejemplo desde el cliente:
functionloadUsers(){google.script.run.withSuccessHandler((users)=>{console.log('Usuarios:',users);}).withFailureHandler((error)=>{console.error('Error:',error);}).getAllRecords('users');}Usar Google Sheets como base de datos funciona bien para proyectos internos, MVPs y automatizaciones, pero tiene límites:
- No reemplaza una base de datos relacional real.
- Las búsquedas grandes pueden ser lentas si la hoja crece mucho.
- No hay relaciones, índices ni transacciones reales.
- Hay que cuidar la concurrencia si varios usuarios escriben al mismo tiempo.
- Agrega una hoja por entidad:
users,orders,products. - Crea funciones genéricas como las de arriba y luego wrappers por entidad.
- Si habrá escrituras simultáneas, considera
LockService. - Si necesitas auditoría, agrega columnas como
createdAt,updatedAt,createdBy.
Una buena evolución para esta plantilla sería mover el CRUD a un archivo como src/server/sheets.gs o Code.js dividido por responsabilidades, y crear en el frontend una capa cliente que encapsule google.script.run.