Node.js: populando uma planilha no Google Sheets
Olá meus Unicórnios! 🦄✨
Semana passada eu precisei de uma coisa que parecia bobinha: pegar uma lista de dados que já existia no meu sistema e jogar numa planilha do Google, para outras pessoas conseguirem olhar e mexer sem precisar entrar em lugar nenhum. Planilha todo mundo sabe usar. 📊
Achei que fosse meia hora. 😅 A parte de escrever na planilha é realmente rápida — são umas quinze linhas. O que me tomou a tarde foram três detalhes que não dão erro nenhum: o código roda liso, imprime "deu certo", e a planilha fica errada. Sem exceção, sem aviso, sem nada vermelho no terminal.
São esses três que eu quero te contar hoje, porque o resto você acha em qualquer documentação.
🔑 Primeiro: a conta de serviço
Para um programa escrever numa planilha sua, ele precisa de uma identidade própria. Isso se chama conta de serviço — pense nela como um robozinho que tem e-mail próprio e a quem você dá permissão, igualzinho a como você compartilharia com uma pessoa.
O caminho é esse, e é feito uma vez só:
- No Google Cloud Console, crie um projeto.
- Ative a Google Sheets API nesse projeto.
- Crie uma conta de serviço e gere uma chave no formato JSON. O navegador vai baixar um arquivinho.
- Abra esse JSON e procure o campo
client_email. É um e-mail com cara estranha, terminando em.iam.gserviceaccount.com. - Abra a sua planilha e compartilhe com esse e-mail, como Editor.
E o ID da planilha sai da própria URL, aquele monte de letras entre /d/ e /edit:
https://docs.google.com/spreadsheets/d/AQUI_ESTA_O_ID/edit
📦 A única dependência
Uma só, e é a oficial do Google:
npm install googleapis
O arquivo da chave JSON e o ID da planilha eu leio de variáveis de ambiente, nunca escritos dentro do código. Aquele JSON é uma credencial de verdade: quem tem ele escreve na sua planilha. Ele nunca vai para o Git.
const { existsSync } = require('fs');
const { google } = require('googleapis');
// As duas configurações vêm do ambiente, nunca escritas aqui dentro.
const ID_DA_PLANILHA = process.env.PLANILHA_ID;
const ARQUIVO_DA_CHAVE = process.env.PLANILHA_CHAVE;
// O nome da aba, exatamente como aparece na abinha de baixo do Sheets.
const ABA = 'Planilha1';
🔌 Conectando
A conexão são seis linhas. Você aponta para o arquivo da chave, diz o que quer poder fazer, e pede o cliente do Sheets:
// Conecta na conta de serviço e devolve o objeto que fala com o Sheets.
function conectar() {
const autenticacao = new google.auth.GoogleAuth({
keyFile: ARQUIVO_DA_CHAVE,
// 'spreadsheets' dá leitura E escrita. Com o escopo '.readonly'
// a autenticação passa e só a escrita falha, lá na frente.
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
return google.sheets({ version: 'v4', auth: autenticacao });
}
Repare no scopes. Existe um escopo parecidíssimo chamado spreadsheets.readonly, e ele é uma cilada linda: com ele a autenticação passa normalmente, o programa segue feliz, e só lá na frente, na hora de escrever, você toma o erro. Como o erro chega longe de onde a causa está, dá vontade de procurar bug no lugar errado. Se você vai escrever, é spreadsheets, sem sufixo.
🔗 A primeira armadilha: o link que come o texto
Essa foi a que mais me irritou, porque ela é invisível.
Quando você manda uma coluna com endereços de site, o Google Sheets olha aquele texto, reconhece que parece um link, e transforma em link clicável. Parece simpático. O problema aparece quando alguém exporta essa planilha, ou copia a coluna para outro lugar, ou tenta comparar com a lista original: o que está na célula deixou de ser exatamente o texto que você mandou.
A solução é uma aspa simples na frente. Ela funciona como um "isto aqui é texto puro, não interprete":
// O Sheets transforma texto que parece link num link clicável.
// A aspa simples na frente manda ele tratar como texto puro.
// A aspa não aparece na célula: é uma marca, não um caractere.
function comoTexto(valor) {
if (!valor) return '';
if (valor.startsWith('http://') || valor.startsWith('https://')) {
return "'" + valor;
}
return valor;
}
O detalhe bonito é que a aspa não aparece na célula. Ela não é um caractere que você está adicionando ao valor — é uma marca que o Sheets consome ao gravar. Quem abrir a planilha vê o endereço limpo, igualzinho, só que como texto.
Para eu ter certeza de que isso funcionava mesmo, montei um Google Sheets falso aqui na minha máquina, que aplica a mesma regra do de verdade e me mostra o tipo de cada célula. Rodei o script sem a proteção do comoTexto:
C1 texto Link
C2 LINK https://exemplo.com/caneca
C3 LINK https://exemplo.com/caderno
C4 LINK https://exemplo.com/adesivo
Três células viraram LINK. Agora com a aspa de volta, mesmo script, mesmos dados:
C1 texto Link
C2 texto https://exemplo.com/caneca
C3 texto https://exemplo.com/caderno
C4 texto https://exemplo.com/adesivo
Mesmo endereço na tela, natureza completamente diferente por baixo. 🎯
👻 A segunda armadilha: as linhas fantasma
Essa é a que dá medo, porque ela não estraga a aparência — ela inventa dado.
O update do Sheets sobrescreve só as células que você mandar. Ele não faz ideia de que aquilo era para ser "a planilha inteira". Se ontem o seu programa escreveu 6 produtos e hoje escreve 2, as células das linhas 4 a 7 continuam exatamente como estavam.
E o resultado é o pior tipo de erro: uma planilha com 7 linhas, todas bem formatadas, onde as 4 primeiras são de hoje e as 3 últimas são de ontem. Ninguém olha para aquilo e desconfia.
Montei essa situação exata para ver o estrago. Primeiro, seis produtos:
Ontem, 6 produtos:
linha 1: Produto Estoque
linha 2: Caneca 12
linha 3: Caderno 7
linha 4: Adesivo 130
linha 5: Chaveiro 4
linha 6: Bloco 20
linha 7: Marcador 9
Agora o estoque acabou e só sobraram dois produtos. Escrevendo sem limpar antes:
Hoje, so 2 produtos — SEM clear:
linha 1: Produto Estoque
linha 2: Caneca 12
linha 3: Caderno 7
linha 4: Adesivo 130
linha 5: Chaveiro 4
linha 6: Bloco 20
linha 7: Marcador 9
Olha o tamanho do problema: a planilha continua com sete linhas. Quatro produtos que não existem mais estão ali, bonitinhos, com estoque e tudo. Se alguém tirar um relatório dessa planilha, o relatório vai estar errado — e vai parecer certo. 😳
A correção é uma chamada a mais, antes da escrita:
// Limpar as colunas inteiras antes de escrever. O update só sobrescreve
// as células que você manda: se ontem havia 50 linhas e hoje há 4,
// as 46 antigas continuariam lá embaixo, com cara de dado atual.
await sheets.spreadsheets.values.clear({
spreadsheetId: ID_DA_PLANILHA,
range: ABA + '!A:D',
});
E aí sim:
Hoje, so 2 produtos — COM clear:
linha 1: Produto Estoque
linha 2: Caneca 12
linha 3: Caderno 7
Repare que eu limpo A:D — as colunas inteiras, sem número de linha. É de propósito: eu não sei quantas linhas sobraram da última vez, então não adianta limpar um intervalo do tamanho do que estou escrevendo agora. Tem que limpar tudo o que aquelas colunas puderem ter.
📐 A terceira armadilha: RAW ou USER_ENTERED
Na hora de escrever, existe um campo chamado valueInputOption, e ele é obrigatório. Ele decide se o Google vai respeitar o que você mandou ou se vai tentar adivinhar o que você quis dizer.
RAW— grava exatamente o que você mandou. Texto é texto, número é número, ponto final.USER_ENTERED— o Sheets interpreta como se uma pessoa tivesse digitado ali. Um=SOMA(A1:A2)vira fórmula de verdade; um01/02vira data; um0800perde o zero da frente e vira o número 800.
Para popular planilha com dado que veio de sistema, RAW é quase sempre o que você quer. O dado já está certo — deixa ele em paz.
// O intervalo tem de acompanhar o tamanho real da matriz.
const intervalo = ABA + '!A1:D' + linhas.length;
await sheets.spreadsheets.values.update({
spreadsheetId: ID_DA_PLANILHA,
range: intervalo,
// RAW grava exatamente o que você mandou.
// USER_ENTERED faria o Sheets interpretar: '=SOMA(A1:A2)' viraria
// fórmula e '01/02' viraria data.
valueInputOption: 'RAW',
requestBody: { values: linhas },
});
E repare no intervalo, logo acima: ele é montado com linhas.length, o tamanho real da matriz. Chutar um número fixo ali é como deixar uma pegadinha armada para daqui a três meses, quando a lista crescer.
🚀 O script inteiro
São 112 linhas, uma dependência, e dá para ler de cima para baixo. Salve como planilha.js:
// Popula uma planilha do Google Sheets a partir de uma lista de dados.
// Rode com: node planilha.js
const { existsSync } = require('fs');
const { google } = require('googleapis');
// As duas configurações vêm do ambiente, nunca escritas aqui dentro.
const ID_DA_PLANILHA = process.env.PLANILHA_ID;
const ARQUIVO_DA_CHAVE = process.env.PLANILHA_CHAVE;
// O nome da aba, exatamente como aparece na abinha de baixo do Sheets.
const ABA = 'Planilha1';
// Os dados que vamos escrever. Numa vida real viriam de um banco,
// de um CSV ou de uma API — aqui são fixos para o exemplo rodar sozinho.
const produtos = [
{ nome: 'Caneca de unicórnio', preco: 39.9, site: 'https://exemplo.com/caneca', estoque: 12 },
{ nome: 'Caderno arco-íris', preco: 24.5, site: 'https://exemplo.com/caderno', estoque: 7 },
{ nome: 'Adesivo brilhante', preco: 8, site: 'https://exemplo.com/adesivo', estoque: 130 },
];
// Conecta na conta de serviço e devolve o objeto que fala com o Sheets.
function conectar() {
const autenticacao = new google.auth.GoogleAuth({
keyFile: ARQUIVO_DA_CHAVE,
// 'spreadsheets' dá leitura E escrita. Com o escopo '.readonly'
// a autenticação passa e só a escrita falha, lá na frente.
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
return google.sheets({ version: 'v4', auth: autenticacao });
}
// O Sheets transforma texto que parece link num link clicável.
// A aspa simples na frente manda ele tratar como texto puro.
// A aspa não aparece na célula: é uma marca, não um caractere.
function comoTexto(valor) {
if (!valor) return '';
if (valor.startsWith('http://') || valor.startsWith('https://')) {
return "'" + valor;
}
return valor;
}
// Monta a matriz de linhas. Cada linha é uma lista, na ordem das colunas.
function montarLinhas() {
const linhas = [];
// Primeira linha: o cabeçalho.
linhas.push(['Produto', 'Preço', 'Link', 'Estoque']);
for (const produto of produtos) {
linhas.push([
produto.nome,
produto.preco,
comoTexto(produto.site),
produto.estoque,
]);
}
return linhas;
}
// Apaga o que estava lá e escreve as linhas novas.
async function popularPlanilha() {
const sheets = conectar();
const linhas = montarLinhas();
// Limpar as colunas inteiras antes de escrever. O update só sobrescreve
// as células que você manda: se ontem havia 50 linhas e hoje há 4,
// as 46 antigas continuariam lá embaixo, com cara de dado atual.
await sheets.spreadsheets.values.clear({
spreadsheetId: ID_DA_PLANILHA,
range: ABA + '!A:D',
});
// O intervalo tem de acompanhar o tamanho real da matriz.
const intervalo = ABA + '!A1:D' + linhas.length;
await sheets.spreadsheets.values.update({
spreadsheetId: ID_DA_PLANILHA,
range: intervalo,
// RAW grava exatamente o que você mandou.
// USER_ENTERED faria o Sheets interpretar: '=SOMA(A1:A2)' viraria
// fórmula e '01/02' viraria data.
valueInputOption: 'RAW',
requestBody: { values: linhas },
});
return linhas.length - 1;
}
async function main() {
if (!ID_DA_PLANILHA || !ARQUIVO_DA_CHAVE) {
console.error('Defina PLANILHA_ID e PLANILHA_CHAVE antes de rodar.');
process.exit(1);
}
if (!existsSync(ARQUIVO_DA_CHAVE)) {
console.error('Não achei o arquivo da chave: ' + ARQUIVO_DA_CHAVE);
process.exit(1);
}
try {
const quantidade = await popularPlanilha();
console.log('Planilha populada com ' + quantidade + ' produtos.');
} catch (erro) {
console.error('Não consegui escrever na planilha: ' + erro.message);
process.exit(1);
}
}
main();
Para rodar, as duas variáveis e pronto:
export PLANILHA_ID="o_id_que_esta_na_url"
export PLANILHA_CHAVE="./minha-chave.json"
node planilha.js
E a saída, quando dá tudo certo:
Planilha populada com 3 produtos.
🧯 Quando dá errado
O try/catch lá do fim existe por um motivo bem prático: os erros do Google chegam com mensagem longa e cheia de detalhe de rede, e quem está rodando o script quer saber é o que fazer. Eu prefiro uma linha em português e sair com código 1:
try {
const quantidade = await popularPlanilha();
console.log('Planilha populada com ' + quantidade + ' produtos.');
} catch (erro) {
console.error('Não consegui escrever na planilha: ' + erro.message);
process.exit(1);
}
As duas conferências antes disso também são de gente que já apanhou. Sem elas, esquecer de definir a variável de ambiente dá um erro lá dentro da biblioteca do Google, num ponto que não te ajuda em nada:
Defina PLANILHA_ID e PLANILHA_CHAVE antes de rodar.
Não achei o arquivo da chave: ./nao-existe.json
Três palavras de aviso valem mais que uma pilha de stack trace. 🙏
💛 O que eu levo disso
As três armadilhas têm a mesma cara, e é isso que as torna perigosas: nenhuma delas dá erro. O programa roda, o terminal diz que deu certo, e o defeito fica na planilha esperando alguém confiar nela.
Por isso, quando eu mexo com Sheets, eu não confio no "rodou sem erro". Eu abro a planilha e olho: o endereço está clicável quando não devia? Sobrou linha de ontem lá embaixo? Aquele código que começa com zero perdeu o zero? São trinta segundos que evitam um relatório errado. ✨
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Leia também
Node.js: alterando o plano de energia do Windows
Como ler e trocar o plano de energia do Windows com Node.js e powercfg: por que resolver por GUID, e o acento que o TextDecoder nao decodifica.
Node.js: exportando grandes volumes do Elasticsearch
Como exportar milhões de registros do Elasticsearch para CSV com Node.js: a parede dos 10.000, a Scroll API e o scroll que fica aberto.
Node.js: gerando certificado SSL e instalando no IIS
Como emitir um certificado gratuito do Let's Encrypt com Node.js e instalar no IIS do Windows, com as armadilhas que travaram tudo.