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.
const DB_CONFIG = {
spreadsheetId: 'REEMPLAZA_CON_TU_SPREADSHEET_ID',
};
function getDb() {
return SpreadsheetApp.openById(DB_CONFIG.spreadsheetId);
}
function getTable(tableName) {
const sheet = getDb().getSheetByName(tableName);
if (!sheet) {
throw new Error(`La hoja "${tableName}" no existe.`);
}
return sheet;
}
function getHeaders(sheet) {
const lastColumn = sheet.getLastColumn();
if (lastColumn === 0) {
return [];
}
return sheet.getRange(1, 1, 1, lastColumn).getValues()[0];
}
function getRows(sheet) {
const lastRow = sheet.getLastRow();
const lastColumn = sheet.getLastColumn();
if (lastRow < 2 || lastColumn === 0) {
return [];
}
return sheet.getRange(2, 1, lastRow - 1, lastColumn).getValues();
}
function mapRow(headers, row) {
return headers.reduce((record, header, index) => {
record[header] = row[index];
return record;
}, {});
}
function mapRows(headers, rows) {
return rows.map((row) => mapRow(headers, row));
}Inserta un nuevo registro al final de la hoja.
function insertRecord(tableName, payload) {
const sheet = getTable(tableName);
const headers = getHeaders(sheet);
if (!headers.length) {
throw new Error(`La hoja "${tableName}" no tiene encabezados.`);
}
const record = {
id: payload.id || Utilities.getUuid(),
createdAt: payload.createdAt || new Date().toISOString(),
...payload,
};
const row = headers.map((header) => {
return header in record ? record[header] : '';
});
sheet.appendRow(row);
return record;
}Ejemplo:
function createUser() {
return insertRecord('users', {
name: 'Ana',
email: 'ana@email.com',
});
}Obtiene todos los registros de una hoja.
function getAllRecords(tableName) {
const sheet = getTable(tableName);
const headers = getHeaders(sheet);
const rows = getRows(sheet);
return mapRows(headers, rows);
}Ejemplo:
function listUsers() {
return getAllRecords('users');
}Busca un registro por su id.
function getRecordById(tableName, id) {
const records = getAllRecords(tableName);
return records.find((record) => String(record.id) === String(id)) || null;
}Ejemplo:
function findUser() {
return getRecordById('users', 'usr_001');
}Actualiza una fila existente buscando por id.
function updateRecord(tableName, id, payload) {
const sheet = getTable(tableName);
const headers = getHeaders(sheet);
const rows = getRows(sheet);
const rowIndex = rows.findIndex((row) => String(row[0]) === String(id));
if (rowIndex === -1) {
throw new Error(`No se encontró un registro con id "${id}".`);
}
const currentRecord = mapRow(headers, rows[rowIndex]);
const nextRecord = {
...currentRecord,
...payload,
id: currentRecord.id,
};
const nextRow = headers.map((header) => {
return header in nextRecord ? nextRecord[header] : '';
});
sheet.getRange(rowIndex + 2, 1, 1, headers.length).setValues([nextRow]);
return nextRecord;
}Ejemplo:
function editUser() {
return updateRecord('users', 'usr_001', {
name: 'Ana Maria',
});
}Elimina una fila por id.
function deleteRecord(tableName, id) {
const sheet = getTable(tableName);
const rows = getRows(sheet);
const rowIndex = rows.findIndex((row) => String(row[0]) === String(id));
if (rowIndex === -1) {
throw new Error(`No se encontró un registro con id "${id}".`);
}
sheet.deleteRow(rowIndex + 2);
return { success: true, id };
}Ejemplo:
function removeUser() {
return deleteRecord('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:
function loadUsers() {
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.