Pular para o conteúdo
Node.js

Node.js: backup de uma base SQL Server

Paloma Macetko
Ilustracao: um unicornio guarda um bau de cristal com cilindros de dados enquanto uma coruja escriba copia o conteudo para um pergaminho, diante de um castelo com velas flutuantes

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:

Exemplos_SQLServerfBackup no GitHubOs três scripts deste artigo, prontos para clonar e rodar: o backup, a listagem das bases e a verificação do arquivo gerado.github.com

🎯 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:

ValorEdiçãoTem WITH COMPRESSION?
2Standard / Web / BISim (desde a 2008 R2)
3Enterprise / DeveloperSim
4ExpressNão
5Azure SQL DatabaseSim
8Azure SQL Managed InstanceSim

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.

Exemplos_SQLServerfBackup no GitHubClone, rode npm install e use: backup.js faz a cópia, listar.js mostra as bases e verificar.js confere a integridade do arquivo.github.com
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