Node.js: consultando CPF e CNPJ no Serasa
Olá meus Unicórnios! 🦄✨
Sabe quando você olha para uma tarefa e pensa "ah, isso é só um GET numa API"? 😅 Pois é. Era o que eu pensava quando precisei consultar CPF e CNPJ no Serasa Experian — puxar a pontuação de crédito e as pendências de alguém antes de fechar um contrato.
E é um GET, sim. Mas entre o "é só um GET" e o "está funcionando" tem uma lista de detalhes que a documentação não conta com todas as letras: o documento não vai na URL, o erro chega em dois formatos diferentes, e — o meu favorito, o que me custou mais tempo — a consulta pode responder HTTP 200 dizendo absolutamente nada.
Este artigo é o caminho inteiro em Node.js puro: login, token, consulta e leitura do retorno. Sem biblioteca nenhuma, porque o fetch já vem embutido no Node desde a versão 18. 🎉
🔑 Primeiro o login, depois a consulta
A API do Serasa trabalha em dois passos, e não tem como pular o primeiro:
POST /security/iam/v1/client-identities/login -> accessToken
GET /credit-services/.../creditreport -> o relatorio
O login é HTTP Basic: usuário e senha codificados em base64 no cabeçalho Authorization. E aqui vai o primeiro detalhe que engana — o corpo do POST vai vazio. É intuitivo tentar mandar as credenciais num JSON no corpo, e não é assim que funciona: elas viajam no cabeçalho, e o corpo é uma string vazia mesmo.
// O login e HTTP Basic: usuario e senha em base64 no cabecalho Authorization.
// O corpo do POST vai VAZIO de proposito - as credenciais nao vao no corpo.
async function fazerLogin() {
const usuario = process.env.SERASA_USUARIO;
const senha = process.env.SERASA_SENHA;
if (!usuario || !senha) {
throw new Error("Defina SERASA_USUARIO e SERASA_SENHA no ambiente.");
}
const basico = Buffer.from(usuario + ":" + senha).toString("base64");
const resposta = await fetch(BASE + "/security/iam/v1/client-identities/login", {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
authorization: "Basic " + basico,
},
body: "",
signal: AbortSignal.timeout(TEMPO_LIMITE),
});
const corpo = await resposta.json();
if (!resposta.ok) {
throw new Error("Login recusado: " + lerMensagemDeErro(corpo, resposta.status));
}
Repare que as credenciais saem de process.env. Isso não é frescura de segurança: usuário e senha do Serasa dão acesso a consultar dado de pessoa e a gastar dinheiro na sua conta. Nunca, em hipótese nenhuma, deixe isso escrito no meio do arquivo — nem "só para testar agora e trocar depois", porque o "depois" tem um jeito curioso de virar um commit no repositório. 😬
🧹 Limpando o documento antes de enviar
O Serasa quer o documento só com dígitos. Se você mandar 529.982.247-25 com a pontuação, a consulta não acha ninguém. Uma linha resolve:
// Tira ponto, traco, barra e espaco. O Serasa quer so os digitos.
function limparDocumento(documento) {
return String(documento).replace(/\D/g, "");
}
O \D é "tudo que não for dígito" — então ponto, traço, barra e espaço somem de uma vez. Simples, mas tem uma armadilha grudada nele, e essa é traiçoeira de verdade. 🙈
É o tipo de bug que passa despercebido por meses, porque só quebra para uma parte dos seus clientes. Todos os outros funcionam lindamente e ninguém desconfia de nada.
Com o documento limpo, dá para decidir o tipo pelo tamanho — e é essa decisão que escolhe qual endpoint chamar depois:
// CPF tem 11 digitos, CNPJ tem 14. Passe SEMPRE texto, nunca numero:
// Number("01234567890") vira 1234567890 e o zero da frente some.
function tipoDoDocumento(documento) {
const limpo = limparDocumento(documento);
if (limpo.length === 11) {
return "CPF";
}
if (limpo.length === 14) {
return "CNPJ";
}
return "INVALIDO";
🧮 Confira o dígito verificador antes de gastar consulta
Essa parte não é exigência do Serasa, é economia. Como toda consulta é cobrada, inclusive a que não encontra ninguém, checar o dígito verificador aqui na sua máquina — de graça, em microssegundos — evita pagar por um documento que já dava para saber que estava errado.
A conta do CPF é conhecida: dois dígitos, cada um calculado sobre os anteriores com pesos decrescentes.
// Digito verificador do CPF. Vale a pena conferir antes de gastar consulta:
// documento errado tambem e cobrado.
function cpfEhValido(documento) {
const cpf = limparDocumento(documento);
if (cpf.length !== 11) {
return false;
}
// "11111111111" passa na conta dos digitos, mas nao existe. Barre na mao.
if (/^(\d)\1{10}$/.test(cpf)) {
return false;
}
for (let rodada = 0; rodada < 2; rodada++) {
const ate = 9 + rodada;
let soma = 0;
for (let i = 0; i < ate; i++) {
soma = soma + Number(cpf[i]) * (ate + 1 - i);
}
let digito = (soma * 10) % 11;
if (digito === 10) {
digito = 0;
}
if (digito !== Number(cpf[ate])) {
return false;
}
}
Aquele if da expressão regular no meio é obrigatório, e é o detalhe que quase todo mundo esquece: 111.111.111-11 passa na conta dos dígitos. A matemática aprova, a realidade não — sequência repetida não é CPF válido, e precisa ser barrada na mão.
Para CNPJ a lógica é a mesma, mudando os pesos, que vão de 2 a 9 e recomeçam:
}
// Digito verificador do CNPJ. Os pesos vao de 2 a 9 e recomecam.
function cnpjEhValido(documento) {
const cnpj = limparDocumento(documento);
if (cnpj.length !== 14) {
return false;
}
if (/^(\d)\1{13}$/.test(cnpj)) {
return false;
}
for (let rodada = 0; rodada < 2; rodada++) {
const ate = 12 + rodada;
let peso = 2;
let soma = 0;
for (let i = ate - 1; i >= 0; i--) {
soma = soma + Number(cnpj[i]) * peso;
peso = peso + 1;
if (peso > 9) {
peso = 2;
}
}
let digito = soma % 11;
if (digito < 2) {
digito = 0;
} else {
digito = 11 - digito;
}
if (digito !== Number(cnpj[ate])) {
return false;
}
}
📮 O documento vai no cabeçalho, não na URL
Essa foi a que me fez reler a documentação três vezes. 😅 Por instinto a gente procura o documento na URL (?cpf=...) ou no corpo de um POST. Não é nem um nem outro: o documento viaja num cabeçalho chamado X-Document-Id, e a requisição é um GET.
E o caminho muda conforme o tipo — pessoa física e pessoa jurídica têm endpoints e nomes de relatório diferentes:
}
// O documento NAO vai na URL nem no corpo: vai no cabecalho X-Document-Id.
// PF e PJ tem caminho e nome de relatorio diferentes.
async function consultarDocumento(documento) {
const limpo = limparDocumento(documento);
const tipo = tipoDoDocumento(limpo);
if (tipo === "INVALIDO") {
throw new Error("Documento precisa ter 11 (CPF) ou 14 (CNPJ) digitos.");
}
if (tipo === "CPF" && !cpfEhValido(limpo)) {
throw new Error("CPF com digito verificador invalido.");
}
if (tipo === "CNPJ" && !cnpjEhValido(limpo)) {
throw new Error("CNPJ com digito verificador invalido.");
}
const token = await fazerLogin();
let caminho;
if (tipo === "CPF") {
caminho =
"/credit-services/person-information-report/v1/creditreport" +
"?reportName=RELATORIO_AVANCADO_TOP_SCORE_PF_PME";
} else {
caminho =
"/credit-services/business-information-report/v1/reports" +
"?reportName=RELATORIO_AVANCADO_PJ_PME";
}
const resposta = await fetch(BASE + caminho, {
method: "GET",
headers: {
accept: "application/json",
authorization: "Bearer " + token,
"X-Document-Id": limpo,
},
signal: AbortSignal.timeout(TEMPO_LIMITE),
});
const corpo = await resposta.json();
if (!resposta.ok) {
O reportName na query string é o que define qual relatório você está comprando. Nome errado ali costuma responder erro de autorização, o que manda você caçar problema de credencial quando o problema era só o nome do produto. 🤦♀️
🎭 O erro chega em dois formatos diferentes
Aqui está uma das coisas que só se descobre apanhando. Quando dá errado, o Serasa às vezes devolve um objeto com message, e às vezes devolve uma lista de objetos com message:
{ "message": "..." } <- as vezes assim
[ { "message": "..." } ] <- e as vezes assim
Se você ler só um dos dois, metade dos erros vira undefined na sua tela e você fica sem a menor pista do que aconteceu. A função que cobre os dois casos é bem chata e bem curta:
throw new Error("Login respondeu 200 mas sem accessToken.");
}
return corpo.accessToken;
}
// O erro do Serasa chega em dois formatos: as vezes um objeto com "message",
// as vezes uma LISTA de objetos. Ler so um dos dois deixa o erro virar
// "undefined" e voce fica sem saber o que aconteceu.
function lerMensagemDeErro(corpo, status) {
if (Array.isArray(corpo) && corpo.length > 0 && corpo[0].message) {
return corpo[0].message;
E tem uma mensagem específica que merece tratamento próprio, porque ela mente sobre o que está acontecendo:
[ERROR][USER-NOT-AUTHORIZED] Transacoes negadas: [BPCA]
Lendo isso, qualquer pessoa vai conferir usuário, senha, token, permissão de perfil. Eu conferi. Não é nada disso: na prática, essa mensagem quase sempre significa fatura em aberto na conta do Serasa. O acesso está certo, o que está faltando é pagamento. Por isso o código traduz a mensagem para algo que aponta para o lugar certo:
// Esta mensagem parece problema de permissao, mas quase sempre e
// fatura em aberto na conta do Serasa.
if (mensagem.indexOf("USER-NOT-AUTHORIZED") !== -1) {
throw new Error("Consulta negada. Verifique o pagamento da conta Serasa.");
}
throw new Error(mensagem);
}
const relatorio = corpo.reports && corpo.reports[0];
// ARMADILHA: HTTP 200 com relatorio vazio. O documento nao foi encontrado,
😱 O 200 que não quer dizer sucesso
Se você só for ler um pedaço deste artigo, leia este. 🙏
Quando o documento não existe na base, o Serasa não responde 404. Ele responde HTTP 200, com um JSON que tem a estrutura toda montadinha — e vazia por dentro. O reports[0] chega lá, direitinho, sem reportName, sem nada.
O estrago disso é grande: resposta.ok é true, seu try não estoura, e o código segue feliz contando pendências num objeto vazio. Resultado? Zero pendências. Ou seja: "documento não encontrado" se disfarça de "nada consta", que é exatamente o oposto — um é ausência de informação, o outro é uma aprovação. 😳
// "nao encontrado" como se fosse "nada consta".
if (!relatorio || !relatorio.reportName) {
throw new Error("Documento nao encontrado na base do Serasa.");
}
return relatorio;
}
Esse if de três linhas é a diferença entre aprovar um contrato com informação e aprovar um contrato com silêncio.
📊 Lendo o resultado: as listas vêm vazias, não ausentes
As restrições vêm agrupadas dentro de negativeData, cada uma com um nome próprio. Um relatório limpo traz todas as listas presentes e vazias — e não ausentes, que é uma diferença importante na hora de contar:
// por isso o "|| []": sem ele um campo que nao veio quebra o .length.
function resumirRestricoes(relatorio) {
const negativos = relatorio.negativeData || {};
function contar(grupo, lista) {
if (!negativos[grupo]) {
return 0;
}
return (negativos[grupo][lista] || []).length;
}
const resumo = {
pefin: contar("pefin", "pefinResponse"),
refin: contar("refin", "refinResponse"),
protestos: contar("notary", "notaryResponse"),
cheques: contar("check", "checkResponse"),
dividas: contar("collectionRecords", "collectionRecordsResponse"),
};
resumo.total =
resumo.pefin + resumo.refin + resumo.protestos + resumo.cheques + resumo.dividas;
return resumo;
Aquele || [] é a rede de proteção: quando um grupo não vem no retorno, sem ele o .length estoura com Cannot read properties of undefined e derruba a consulta inteira por causa de um campo que só faltou.
Os nomes dos grupos são os do mercado de crédito, e vale a tradução:
| Campo no JSON | O que é |
|---|---|
pefin | Dívida vencida com instituição financeira |
refin | Dívida vencida já renegociada e vencida de novo |
notary | Protesto em cartório |
check | Cheque sem fundo |
collectionRecords | Dívida em cobrança |
score.score | A pontuação de crédito, de 0 a 1000 |
⏱️ O relatório é lento — não aperte o timeout
Esse relatório demora. Não é uma consulta de chave-valor: o Serasa monta o documento consolidando várias fontes, e um minuto de espera é normal. Prepare-se para esperar. 😴
const TEMPO_LIMITE = 60000; // 60s. O relatorio e lento, nao diminua isso.
A tentação de baixar isso para 10 segundos "porque nenhuma API decente demora tanto" é grande, e o preço é caro: você paga a consulta (ela roda lá do lado deles), aborta antes da resposta chegar e fica sem o dado. Pagou e não levou.
Um detalhe do fetch ao usar AbortSignal.timeout(): o estouro chega no catch como um erro de nome TimeoutError, com uma mensagem genérica que não ajuda ninguém. Vale traduzir:
console.log("Situacao: " + restricoes.total + " restricao(oes)");
}
} catch (erro) {
// O timeout do fetch chega como AbortError, com mensagem pouco util.
if (erro.name === "TimeoutError" || erro.name === "AbortError") {
console.error(
"Erro: o Serasa demorou mais de " + TEMPO_LIMITE / 1000 + "s para responder."
);
📄 O arquivo inteiro
É este o script completo, do jeito que ele roda — um arquivo só, de cima para baixo, sem nenhuma dependência externa:
// Consulta de CPF / CNPJ no Serasa Experian.
// Node.js 18 ou mais novo (usa o fetch embutido). Nenhuma dependencia externa.
//
// As credenciais saem SEMPRE de variavel de ambiente. Nunca escreva
// usuario e senha aqui dentro.
//
// SERASA_USUARIO=... SERASA_SENHA=... node serasa.js 12345678909
// Ambiente de homologacao ("uat-") e producao tem endereco diferente.
// Comece pelo de homologacao: o de producao cobra por consulta.
const BASE = process.env.SERASA_BASE || "https://uat-api.serasaexperian.com.br";
const TEMPO_LIMITE = 60000; // 60s. O relatorio e lento, nao diminua isso.
// Tira ponto, traco, barra e espaco. O Serasa quer so os digitos.
function limparDocumento(documento) {
return String(documento).replace(/\D/g, "");
}
// CPF tem 11 digitos, CNPJ tem 14. Passe SEMPRE texto, nunca numero:
// Number("01234567890") vira 1234567890 e o zero da frente some.
function tipoDoDocumento(documento) {
const limpo = limparDocumento(documento);
if (limpo.length === 11) {
return "CPF";
}
if (limpo.length === 14) {
return "CNPJ";
}
return "INVALIDO";
}
// Digito verificador do CPF. Vale a pena conferir antes de gastar consulta:
// documento errado tambem e cobrado.
function cpfEhValido(documento) {
const cpf = limparDocumento(documento);
if (cpf.length !== 11) {
return false;
}
// "11111111111" passa na conta dos digitos, mas nao existe. Barre na mao.
if (/^(\d)\1{10}$/.test(cpf)) {
return false;
}
for (let rodada = 0; rodada < 2; rodada++) {
const ate = 9 + rodada;
let soma = 0;
for (let i = 0; i < ate; i++) {
soma = soma + Number(cpf[i]) * (ate + 1 - i);
}
let digito = (soma * 10) % 11;
if (digito === 10) {
digito = 0;
}
if (digito !== Number(cpf[ate])) {
return false;
}
}
return true;
}
// Digito verificador do CNPJ. Os pesos vao de 2 a 9 e recomecam.
function cnpjEhValido(documento) {
const cnpj = limparDocumento(documento);
if (cnpj.length !== 14) {
return false;
}
if (/^(\d)\1{13}$/.test(cnpj)) {
return false;
}
for (let rodada = 0; rodada < 2; rodada++) {
const ate = 12 + rodada;
let peso = 2;
let soma = 0;
for (let i = ate - 1; i >= 0; i--) {
soma = soma + Number(cnpj[i]) * peso;
peso = peso + 1;
if (peso > 9) {
peso = 2;
}
}
let digito = soma % 11;
if (digito < 2) {
digito = 0;
} else {
digito = 11 - digito;
}
if (digito !== Number(cnpj[ate])) {
return false;
}
}
return true;
}
// O login e HTTP Basic: usuario e senha em base64 no cabecalho Authorization.
// O corpo do POST vai VAZIO de proposito - as credenciais nao vao no corpo.
async function fazerLogin() {
const usuario = process.env.SERASA_USUARIO;
const senha = process.env.SERASA_SENHA;
if (!usuario || !senha) {
throw new Error("Defina SERASA_USUARIO e SERASA_SENHA no ambiente.");
}
const basico = Buffer.from(usuario + ":" + senha).toString("base64");
const resposta = await fetch(BASE + "/security/iam/v1/client-identities/login", {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
authorization: "Basic " + basico,
},
body: "",
signal: AbortSignal.timeout(TEMPO_LIMITE),
});
const corpo = await resposta.json();
if (!resposta.ok) {
throw new Error("Login recusado: " + lerMensagemDeErro(corpo, resposta.status));
}
if (!corpo.accessToken) {
throw new Error("Login respondeu 200 mas sem accessToken.");
}
return corpo.accessToken;
}
// O erro do Serasa chega em dois formatos: as vezes um objeto com "message",
// as vezes uma LISTA de objetos. Ler so um dos dois deixa o erro virar
// "undefined" e voce fica sem saber o que aconteceu.
function lerMensagemDeErro(corpo, status) {
if (Array.isArray(corpo) && corpo.length > 0 && corpo[0].message) {
return corpo[0].message;
}
if (corpo && corpo.message) {
return corpo.message;
}
return "HTTP " + status;
}
// O documento NAO vai na URL nem no corpo: vai no cabecalho X-Document-Id.
// PF e PJ tem caminho e nome de relatorio diferentes.
async function consultarDocumento(documento) {
const limpo = limparDocumento(documento);
const tipo = tipoDoDocumento(limpo);
if (tipo === "INVALIDO") {
throw new Error("Documento precisa ter 11 (CPF) ou 14 (CNPJ) digitos.");
}
if (tipo === "CPF" && !cpfEhValido(limpo)) {
throw new Error("CPF com digito verificador invalido.");
}
if (tipo === "CNPJ" && !cnpjEhValido(limpo)) {
throw new Error("CNPJ com digito verificador invalido.");
}
const token = await fazerLogin();
let caminho;
if (tipo === "CPF") {
caminho =
"/credit-services/person-information-report/v1/creditreport" +
"?reportName=RELATORIO_AVANCADO_TOP_SCORE_PF_PME";
} else {
caminho =
"/credit-services/business-information-report/v1/reports" +
"?reportName=RELATORIO_AVANCADO_PJ_PME";
}
const resposta = await fetch(BASE + caminho, {
method: "GET",
headers: {
accept: "application/json",
authorization: "Bearer " + token,
"X-Document-Id": limpo,
},
signal: AbortSignal.timeout(TEMPO_LIMITE),
});
const corpo = await resposta.json();
if (!resposta.ok) {
const mensagem = lerMensagemDeErro(corpo, resposta.status);
// Esta mensagem parece problema de permissao, mas quase sempre e
// fatura em aberto na conta do Serasa.
if (mensagem.indexOf("USER-NOT-AUTHORIZED") !== -1) {
throw new Error("Consulta negada. Verifique o pagamento da conta Serasa.");
}
throw new Error(mensagem);
}
const relatorio = corpo.reports && corpo.reports[0];
// ARMADILHA: HTTP 200 com relatorio vazio. O documento nao foi encontrado,
// mas o Serasa responde 200 mesmo assim. Sem este if, voce trata
// "nao encontrado" como se fosse "nada consta".
if (!relatorio || !relatorio.reportName) {
throw new Error("Documento nao encontrado na base do Serasa.");
}
return relatorio;
}
// Conta as restricoes. Um relatorio limpo traz as listas VAZIAS, nao ausentes -
// por isso o "|| []": sem ele um campo que nao veio quebra o .length.
function resumirRestricoes(relatorio) {
const negativos = relatorio.negativeData || {};
function contar(grupo, lista) {
if (!negativos[grupo]) {
return 0;
}
return (negativos[grupo][lista] || []).length;
}
const resumo = {
pefin: contar("pefin", "pefinResponse"),
refin: contar("refin", "refinResponse"),
protestos: contar("notary", "notaryResponse"),
cheques: contar("check", "checkResponse"),
dividas: contar("collectionRecords", "collectionRecordsResponse"),
};
resumo.total =
resumo.pefin + resumo.refin + resumo.protestos + resumo.cheques + resumo.dividas;
return resumo;
}
async function main() {
const documento = process.argv[2];
if (!documento) {
console.log("Uso: node serasa.js <cpf ou cnpj>");
return;
}
try {
const relatorio = await consultarDocumento(documento);
const restricoes = resumirRestricoes(relatorio);
const score = (relatorio.score && relatorio.score.score) || 0;
console.log("Relatorio: " + relatorio.reportName);
console.log("Score: " + score);
console.log("PEFIN: " + restricoes.pefin);
console.log("REFIN: " + restricoes.refin);
console.log("Protestos: " + restricoes.protestos);
console.log("Cheques: " + restricoes.cheques);
console.log("Dividas: " + restricoes.dividas);
if (restricoes.total === 0) {
console.log("Situacao: nada consta");
} else {
console.log("Situacao: " + restricoes.total + " restricao(oes)");
}
} catch (erro) {
// O timeout do fetch chega como AbortError, com mensagem pouco util.
if (erro.name === "TimeoutError" || erro.name === "AbortError") {
console.error(
"Erro: o Serasa demorou mais de " + TEMPO_LIMITE / 1000 + "s para responder."
);
} else {
console.error("Erro: " + erro.message);
}
process.exitCode = 1;
}
}
// Só roda a CLI quando o arquivo é chamado direto. Sem isto, quem der
// require() neste arquivo recebe a mensagem de uso na tela.
if (require.main === module) {
main();
}
Para rodar, as credenciais vão pelo ambiente:
SERASA_USUARIO=seu_usuario SERASA_SENHA=sua_senha node serasa.js 529.982.247-25
🎯 O que sai na tela
Um documento fictício sem nenhuma restrição imprime assim — a pontuação alta e todos os contadores zerados:
Relatorio: RELATORIO_AVANCADO_TOP_SCORE_PF_PME
Score: 782
PEFIN: 0
REFIN: 0
Protestos: 0
Cheques: 0
Dividas: 0
Situacao: nada consta
E um com pendências, onde o interessante é ver a pontuação despencar junto:
Relatorio: RELATORIO_AVANCADO_PJ_PME
Score: 212
PEFIN: 2
REFIN: 1
Protestos: 0
Cheques: 0
Dividas: 0
Situacao: 3 restricao(oes)
Os caminhos de erro são o que mais importa no dia a dia, e cada um diz o que fazer:
Erro: Documento nao encontrado na base do Serasa.
Erro: Consulta negada. Verifique o pagamento da conta Serasa.
Erro: CPF com digito verificador invalido.
Erro: Defina SERASA_USUARIO e SERASA_SENHA no ambiente.
Repare que o do dígito verificador nem chega a sair da sua máquina — ele é barrado antes da chamada, e essa consulta não entra na fatura. 💸
🧷 Três detalhes pequenos que salvam a pele
Fechando com o que eu gostaria de ter lido antes de começar:
Trate documento como texto, sempre. A tentação de guardar CPF como número no banco é real e o prejuízo aparece só nos documentos que começam com zero — tarde demais, e só para alguns clientes.
Comece pelo ambiente de homologação. É o mesmo código, muda só o endereço (uat-api em vez de api), e ali você erra à vontade sem cada tentativa virar linha na fatura.
Guarde o retorno inteiro. O relatório traz muito mais do que a pontuação e os contadores que este script lê, e refazer a consulta para pegar um campo que você já tinha recebido é pagar duas vezes pela mesma informação. Salve o JSON completo e leia dele depois.
No fim das contas, a parte difícil da integração com o Serasa não foi montar a requisição — foi desconfiar do sucesso. Um 200 que não é sucesso, uma mensagem de autorização que fala de pagamento, uma lista vazia que parece aprovação: o retorno da API tem mais opinião do que parece, e ler ele com atenção é o trabalho de verdade. 🔍
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.