Node.js: backup de uma base SQL Server
Olá meus Unicórnios! 🦄✨
Sabe quando você precisa de uma coisa simples — "só faz o backup dessa base aí" — e descobre que o caminho tem três buraquinhos que ninguém te avisou? 😅 Pois é. Hoje eu vou te mostrar como fazer backup de uma base SQL Server usando Node.js, do jeito mais direto possível.
Nada de agendador, nada de nuvem: um script, um comando, um
arquivo .bak no disco. É o tijolo básico — depois
que ele funciona, você pluga onde quiser.
Se você prefere o código antes da explicação, está tudo no GitHub:
🎯 A ideia central: quem escreve o arquivo é o SQL Server
Essa é a primeira coisa a entender, e ela muda tudo. Diferente do MySQL,
onde o mysqldump despeja o conteúdo na saída padrão e você
captura, no SQL Server o backup é um comando que o banco executa:
BACKUP DATABASE [MinhaBase] TO DISK = N'C:\Backups\minha.bak' WITH COMPRESSION, FORMAT, STATS = 10
Repare no detalhe cruel: esse caminho não é o do seu Node.
É o caminho no disco do servidor SQL, escrito pela conta de serviço
dele. Se o seu script roda em outra máquina, o .bak vai
nascer lá, não aqui — e o seu fs.statSync() vai estourar um
ENOENT lindo, apontando para um arquivo que existe, só que do
outro lado da rede. 🤯
Ou seja: o Node aqui não faz o trabalho pesado. Ele é o maestro — manda o comando, escuta o progresso e confere o resultado.
🔌 Conectando: a pegadinha da instância nomeada
Só uma dependência, e ela é a oficial:
npm install mssql
mssql — driver de SQL Server para Node.jsnpmjs.com
Conectar parece trivial, e é — até você topar com um servidor de instância
nomeada, tipo SRV01\SQLEXPRESS. Aí você descobre que
instância nomeada e porta são mutuamente exclusivas na lib:
se você passar as duas, o driver ignora a instância e tenta a porta que você
mandou. E como a instância nomeada quase nunca está na 1433, você toma um
timeout sem entender por quê.
//
// ATENCAO: instancia nomeada e porta sao mutuamente exclusivas. Se voce passar
// as duas, o driver ignora a instancia e tenta a porta — e como a instancia
// nomeada quase nunca escuta na 1433, o resultado e um timeout sem explicacao.
function montarConfig() {
const [servidor, instancia] = HOST.split('\\');
const config = {
server: servidor,
user: USUARIO,
password: SENHA,
options: {
trustServerCertificate: true, // ok em rede interna
encrypt: false, // em rede publica, ligue isto
},
connectionTimeout: 15000, // 15s para ATENDER: se nao atende, e problema
requestTimeout: 0, // 0 = sem limite; backup grande passa de 15s
};
if (instancia) {
config.options.instanceName = instancia; // com instancia: sem porta
} else {
config.port = PORTA; // sem instancia: com porta
}
return config;
}
O if/else do final é a defesa: ou instância, ou
porta — nunca as duas. Sem ele, o backup de qualquer SQL Express
da sua rede falha na conexão.
Os outros dois campos merecem uma palavra. trustServerCertificate: true
e encrypt: false servem para rede interna — script e banco na
mesma máquina ou na mesma LAN. Se o seu Node estiver atravessando rede
pública para chegar no banco, não copie isso: ligue o
encrypt e trate o certificado direito. 🔒
⏱️ O timeout que mata backup grande
Esta aqui é curta e salva o seu dia:
connectionTimeout: 15000, // 15s para ATENDER: se nao atende, e problema
requestTimeout: 0, // 0 = sem limite; backup grande passa de 15s
requestTimeout: 0 significa sem timeout. O
padrão da lib é 15 segundos — ótimo para um SELECT, catastrófico
para um backup. Uma base de 40 GB não termina em 15 segundos nem no seu melhor
dia. 😅
E o erro que você recebe não diz "seu backup demorou demais". Ele diz
RequestError: Timeout: Request failed to complete in 15000ms —
e o pior: o backup continua rodando no servidor. Você só
perdeu a conexão que estava escutando. O arquivo fica lá, e o seu script
jura que falhou.
Repare que o connectionTimeout continua em 15 s, e é de
propósito: se o servidor não atende em 15 segundos, aí é problema
de verdade e não vale a pena esperar.
📊 O progresso vem por um canal que ninguém espera
Você põe STATS = 10 no comando esperando receber o progresso
de volta. E aí roda, e o await fica parado, e não vem nada. Onde
foi parar? 🤔
É que o "37 percent processed." não é resultado da
query. É uma mensagem informativa do servidor — o mesmo canal do
PRINT. No driver mssql, ela chega por um
evento:
let ultimo = -1;
conexao.on('infoMessage', (info) => {
const msg = info.message || String(info);
const pct = lerProgresso(msg);
if (pct === null) {
console.log(` ${msg}`); // outras mensagens do servidor
} else if (pct - ultimo >= 10) {
ultimo = pct;
console.log(` ${pct}%`);
}
});
E o parser, que é só uma expressão regular:
// Le "37 percent processed." e devolve 37.
// Limitamos a 99 de proposito: depois do "100 percent" o servidor ainda faz
// flush dos buffers e fecha o arquivo. Quem anuncia 100% e o fim do comando.
function lerProgresso(mensagem) {
const achou = String(mensagem).match(/(\d+)\s+percent\s+processed/i);
return achou ? Math.min(99, parseInt(achou[1], 10)) : null;
}
O Math.min(99, ...) ali não é frescura: ele trava o
progresso em 99% mesmo quando o servidor manda 100. Entre o "100
percent processed" e o backup realmente terminar ainda tem flush de buffers e
fechamento do arquivo. Mostrar 100% e a barra ficar parada mais 40 segundos é
a pior experiência possível — o usuário acha que travou. Quem declara os 100%
é o await retornando, não o servidor. 🙂
🚫 O WITH COMPRESSION não existe no Express
O WITH COMPRESSION é ótimo — o próprio SQL Server comprime o
.bak enquanto escreve, e você economiza tempo e disco de uma vez
só. Exceto que ele não existe na edição Express. E o SQL
Server não te dá um aviso: ele derruba o comando inteiro com erro.
É por isso que uma coisa que funciona lindamente na sua máquina de desenvolvimento explode no Express do cliente. 😬 A solução é perguntar antes de mandar o comando:
// A edicao Express NAO tem WITH COMPRESSION: usar la derruba o comando
// inteiro com erro. Entao perguntamos antes de montar o comando.
const resposta = await conexao.request()
.query("SELECT SERVERPROPERTY('EngineEdition') AS edicao");
const edicao = resposta.recordset[0].edicao;
const ehExpress = edicao === 4;
// FORMAT sobrescreve o arquivo. Sem ele, o SQL Server ANEXA o backup
// ao final de um .bak que ja exista, e o arquivo cresce sem parar.
const opcoes = ehExpress
? 'FORMAT, STATS = 10'
: 'COMPRESSION, FORMAT, STATS = 10';
console.log(`Edicao : ${edicao}${ehExpress ? ' (Express, sem COMPRESSION)' : ''}`);
console.log(`Opcoes : ${opcoes}`);
O EngineEdition é um número pequeno e estável. Vale a pena
decorar os que importam:
| Valor | Edição | Tem WITH COMPRESSION? |
|---|---|---|
| 2 | Standard / Web / BI | Sim (desde a 2008 R2) |
| 3 | Enterprise / Developer | Sim |
| 4 | Express | Não |
| 5 | Azure SQL Database | Sim |
| 8 | Azure SQL Managed Instance | Sim |
E já que falamos das outras opções do WITH: o
FORMAT é o que faz o arquivo ser sobrescrito em
vez de o backup ser anexado ao final de um .bak que já
exista. Sem ele, rodando todo dia no mesmo nome de arquivo, você acaba com um
arquivo gigante contendo N backups empilhados dentro. Como aqui cada execução
gera um nome novo com data e hora, o FORMAT é só um cinto de
segurança. 🎽
🧹 O .bak órfão de 30 GB
Última armadilha, e essa dói no bolso. Se o backup falhar depois
de o SQL Server já ter escrito bastante coisa, o .bak parcial
fica no disco. Ninguém apaga. Na semana seguinte falha de novo, e agora são
dois. 💸
} catch (erro) {
if (conexao) {
try { await conexao.close(); } catch { }
}
// Se o backup falhar no meio, o SQL Server deixa um .bak parcial —
// que pode ter dezenas de GB. Sem esta limpeza, ele fica la para sempre.
try {
if (fs.existsSync(arquivo)) {
fs.unlinkSync(arquivo);
console.log(`Arquivo parcial removido.`);
}
} catch { }
console.error(`ERRO: ${erro.message}`);
throw erro;
Repare que os catch { } vazios aqui são
intencionais, e é o único lugar do script onde eu faço isso. Estamos
no caminho do erro: se o close() ou o unlinkSync()
falharem, engolir é o certo — o erro que importa é o erro
original, e deixar ele ser substituído por um "não consegui fechar a conexão"
é perder a informação de verdade. 🕵️♀️
📜 O script completo
Aqui está inteiro — é o mesmo backup.js que está no
repositório, sem nada omitido:
// backup.js — backup de uma base SQL Server com Node.js
//
// Uso: node backup.js NomeDaBase
//
// Antes de rodar: npm install
// E configure as variaveis de ambiente (veja o .env.example / README.md)
const sql = require('mssql');
const fs = require('fs');
const path = require('path');
// ─────────────────────────────────────────────────────────────────────────────
// Configuracao — vem toda de variavel de ambiente, nada de senha no codigo
// ─────────────────────────────────────────────────────────────────────────────
const HOST = process.env.MSSQL_HOST || 'localhost'; // aceita "SERVIDOR\INSTANCIA"
const USUARIO = process.env.MSSQL_USER || 'sa';
const SENHA = process.env.MSSQL_PASSWORD || '';
const PORTA = Number(process.env.MSSQL_PORT || 1433);
const PASTA_BACKUP = process.env.BACKUP_DIR || 'C:\\Backups';
// ─────────────────────────────────────────────────────────────────────────────
// Funcoes auxiliares
// ─────────────────────────────────────────────────────────────────────────────
// Monta a configuracao de conexao.
//
// ATENCAO: instancia nomeada e porta sao mutuamente exclusivas. Se voce passar
// as duas, o driver ignora a instancia e tenta a porta — e como a instancia
// nomeada quase nunca escuta na 1433, o resultado e um timeout sem explicacao.
function montarConfig() {
const [servidor, instancia] = HOST.split('\\');
const config = {
server: servidor,
user: USUARIO,
password: SENHA,
options: {
trustServerCertificate: true, // ok em rede interna
encrypt: false, // em rede publica, ligue isto
},
connectionTimeout: 15000, // 15s para ATENDER: se nao atende, e problema
requestTimeout: 0, // 0 = sem limite; backup grande passa de 15s
};
if (instancia) {
config.options.instanceName = instancia; // com instancia: sem porta
} else {
config.port = PORTA; // sem instancia: com porta
}
return config;
}
// Nome de arquivo com data e hora: 2026-08-05_14-32-07.bak
// Formato escolhido para ordenar alfabeticamente na ordem cronologica e para
// nao usar ":" , que o Windows nao aceita em nome de arquivo.
function nomeArquivo() {
const d = new Date();
const dd = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${dd(d.getMonth() + 1)}-${dd(d.getDate())}`
+ `_${dd(d.getHours())}-${dd(d.getMinutes())}-${dd(d.getSeconds())}.bak`;
}
// Le "37 percent processed." e devolve 37.
// Limitamos a 99 de proposito: depois do "100 percent" o servidor ainda faz
// flush dos buffers e fecha o arquivo. Quem anuncia 100% e o fim do comando.
function lerProgresso(mensagem) {
const achou = String(mensagem).match(/(\d+)\s+percent\s+processed/i);
return achou ? Math.min(99, parseInt(achou[1], 10)) : null;
}
// ─────────────────────────────────────────────────────────────────────────────
// O backup
// ─────────────────────────────────────────────────────────────────────────────
async function fazerBackup(base) {
// ATENCAO: este caminho e no disco DO SERVIDOR SQL, nao no do Node.
// Se os dois estiverem em maquinas diferentes, o .bak nasce la.
const pasta = path.join(PASTA_BACKUP, base);
const arquivo = path.join(pasta, nomeArquivo());
fs.mkdirSync(pasta, { recursive: true });
console.log('=== BACKUP START ===');
console.log(`Base : ${base}`);
console.log(`Servidor : ${HOST}`);
console.log(`Arquivo : ${arquivo}`);
const inicio = Date.now();
let conexao = null;
try {
conexao = await sql.connect(montarConfig());
// O progresso do STATS nao volta como resultado da query: ele chega
// como mensagem informativa, no mesmo canal do PRINT. Sem este
// listener, o await fica mudo ate o backup terminar.
let ultimo = -1;
conexao.on('infoMessage', (info) => {
const msg = info.message || String(info);
const pct = lerProgresso(msg);
if (pct === null) {
console.log(` ${msg}`); // outras mensagens do servidor
} else if (pct - ultimo >= 10) {
ultimo = pct;
console.log(` ${pct}%`);
}
});
// A edicao Express NAO tem WITH COMPRESSION: usar la derruba o comando
// inteiro com erro. Entao perguntamos antes de montar o comando.
const resposta = await conexao.request()
.query("SELECT SERVERPROPERTY('EngineEdition') AS edicao");
const edicao = resposta.recordset[0].edicao;
const ehExpress = edicao === 4;
// FORMAT sobrescreve o arquivo. Sem ele, o SQL Server ANEXA o backup
// ao final de um .bak que ja exista, e o arquivo cresce sem parar.
const opcoes = ehExpress
? 'FORMAT, STATS = 10'
: 'COMPRESSION, FORMAT, STATS = 10';
console.log(`Edicao : ${edicao}${ehExpress ? ' (Express, sem COMPRESSION)' : ''}`);
console.log(`Opcoes : ${opcoes}`);
// Os colchetes protegem o nome da base; o N'...' e o caminho como texto.
// O replace dobra apostrofos — sem ele, um caminho com ' quebra o comando.
await conexao.request().query(
`BACKUP DATABASE [${base}] ` +
`TO DISK = N'${arquivo.replace(/'/g, "''")}' ` +
`WITH ${opcoes}`
);
await conexao.close();
conexao = null;
const tamanho = fs.statSync(arquivo).size;
const segundos = ((Date.now() - inicio) / 1000).toFixed(1);
console.log(`Tamanho : ${(tamanho / 1024 / 1024).toFixed(2)} MB`);
console.log(`Tempo : ${segundos} s`);
console.log('=== BACKUP COMPLETE ===');
} catch (erro) {
if (conexao) {
try { await conexao.close(); } catch { }
}
// Se o backup falhar no meio, o SQL Server deixa um .bak parcial —
// que pode ter dezenas de GB. Sem esta limpeza, ele fica la para sempre.
try {
if (fs.existsSync(arquivo)) {
fs.unlinkSync(arquivo);
console.log(`Arquivo parcial removido.`);
}
} catch { }
console.error(`ERRO: ${erro.message}`);
throw erro;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Entrada
// ─────────────────────────────────────────────────────────────────────────────
const base = process.argv[2];
if (!base) {
console.error('Uso: node backup.js NomeDaBase');
process.exit(1);
}
fazerBackup(base).catch(() => process.exit(1));
Configure por variáveis de ambiente — nada de senha dentro do arquivo, por favor 🙏:
set MSSQL_HOST=localhost
set MSSQL_USER=sa
set MSSQL_PASSWORD=sua-senha
set BACKUP_DIR=C:\Backups
node backup.js MinhaBase
E a saída fica assim:
=== BACKUP START ===
Base : MinhaBase
Servidor : localhost
Arquivo : C:\Backups\MinhaBase\2026-08-05_14-32-07.bak
Edicao : 3
Opcoes : COMPRESSION, FORMAT, STATS = 10
10%
20%
30%
...
90%
Processed 4728 pages for database 'MinhaBase', file 'MinhaBase' on file 1.
BACKUP DATABASE successfully processed 4731 pages in 0.512 seconds.
Tamanho : 12.84 MB
Tempo : 1.4 s
=== BACKUP COMPLETE ===
✅ Conferindo se o arquivo presta
Backup que ninguém testou é esperança, não é backup. 😬 O SQL Server tem dois comandos baratos para isso, e vale rodar pelo menos o primeiro:
-- Lê o cabeçalho: qual base, quando, qual tamanho
RESTORE HEADERONLY FROM DISK = N'C:\Backups\MinhaBase\2026-08-05_14-32-07.bak'
-- Confere os checksums do arquivo inteiro, sem restaurar nada
RESTORE VERIFYONLY FROM DISK = N'C:\Backups\MinhaBase\2026-08-05_14-32-07.bak'
O VERIFYONLY lê o arquivo todo e confere a integridade, mas
não restaura nem sobrescreve nada — pode rodar tranquila. Só
lembre que ele valida o arquivo, não os dados lá dentro: um backup
íntegro de uma base já corrompida continua íntegro. Para os dados, quem
responde é o DBCC CHECKDB. 🧐
🧪 O que eu testei — e o que não deu para testar
Sendo honesta com você, porque isso importa: a máquina onde escrevi este artigo não tem SQL Server instalado. Então vamos separar o que é confirmado do que não é.
Rodou de verdade aqui, com teste automatizado: a montagem
da config (incluindo a regra da instância nomeada), a escolha da cláusula
WITH por edição, a montagem e o escape do T-SQL, o parser do
percent processed e a geração do nome do arquivo. Doze asserções,
no teste.js do repositório:
Conexao
ok host simples usa a porta
ok instancia nomeada NAO leva porta
Nome do arquivo
ok formato com data e hora
ok sem caractere proibido no Windows
ok ordem alfabetica = ordem cronologica
Progresso
ok le "37 percent processed."
ok trava em 99, nunca 100
ok outras mensagens devolvem null
Comando
ok BACKUP DATABASE montado certo
ok apostrofo no caminho e escapado
ok caminho malicioso nao quebra o comando
ok Express nao recebe COMPRESSION
12 ok, 0 falha(s)
O que não rodou aqui: o BACKUP DATABASE de
ponta a ponta contra um servidor real e a leitura ao vivo do
infoMessage. Esses vêm da experiência de usar esse código em
servidor, não de uma execução nesta máquina — o parser eu testei com as
strings que o servidor manda, não com o servidor mandando. A saída de terminal
lá em cima é ilustrativa, montada a partir do formato real das mensagens.
📦 O repositório
Todo o código deste artigo está no GitHub, com um README que resume as
armadilhas. Além do backup.js que você viu aqui, tem mais dois
scripts pequenos: um que lista as bases do servidor e outro que confere um
.bak já existente.
git clone https://github.com/cmacetko/Exemplos_SQLServerfBackup.git
cd Exemplos_SQLServerfBackup
npm install
node listar.js # quais bases existem
node backup.js MinhaBase # faz o backup
node verificar.js "C:\Backups\MinhaBase\2026-08-05_14-32-07.bak"
Cada script é independente e se lê de cima a baixo, sem require
entre eles — dá para copiar um sozinho que ele funciona. 🙂
🎁 Levando embora
Se você fechar a aba agora, leve estes quatro:
1. O TO DISK é o disco do SQL Server,
não o do seu Node.
2. requestTimeout: 0, ou backup grande morre aos
15 segundos.
3. Progresso chega por conexao.on('infoMessage') —
e trave em 99%.
4. Pergunte o EngineEdition antes de usar
WITH COMPRESSION, porque o Express não tem.
São 176 linhas contando os comentários, e elas resolvem o problema inteiro. Simples é bonito. 💜
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.