Gerar PDF a partir de HTML em Node.js
Guia · atualizado em 12/09/2026 · 8 min de leitura
Precisa gerar PDF a partir de HTML em Node.js? Faturas, relatórios e ingressos costumam nascer como HTML, porque é o jeito mais rápido de desenhar um documento. Este guia compara as bibliotecas mais usadas no ecossistema JavaScript e mostra o código completo com fetch nativo e Express, usando a API do PDF Services.
As opções no ecossistema Node.js
| Opção | Como funciona | Pontos de atenção |
|---|---|---|
| Puppeteer | Controla um Chromium headless e usa page.pdf(). | Baixa um navegador de centenas de MB, consome bastante memória e exige pacotes especiais em serverless (Lambda, Vercel). |
| Playwright | Mesma ideia do Puppeteer, com API moderna. | A geração de PDF só funciona no Chromium; os mesmos custos de rodar um navegador. |
| PDFKit, pdfmake, jsPDF | Constroem o PDF por código, elemento por elemento. | Não convertem HTML de verdade: cada layout precisa ser reescrito na API da biblioteca. |
| html-pdf (PhantomJS) | Wrapper do PhantomJS. | PhantomJS está descontinuado há anos. Evite. |
| API HTTP (PDF Services) | Você envia o HTML e recebe o PDF. | Depende de rede e é cobrado por uso, mas não há navegador para instalar nem memória extra no servidor. |
O PDF Services converte com o iText pdfHTML, um motor maduro de conversão de HTML e CSS para PDF, e todo o código do serviço é aberto sob a licença AGPL v3. Você não precisa instalar o iText, Java ou .NET: a conversão acontece na API.
Se o documento depende de JavaScript rodando na página, como um gráfico desenhado em canvas, um navegador headless é necessário. Para documentos montados no servidor com templates (EJS, Handlebars, Nunjucks, React renderizado para string), uma API de conversão resolve com uma requisição.
Passo a passo com fetch (Node.js 18 ou superior)
1. Crie a conta e a chave
Cadastre a empresa, confirme o e-mail e gere uma chave em Chaves de API no painel. Guarde-a como segredo, em variável de ambiente ou gerenciador de segredos, nunca no repositório.
2. Envie o HTML e salve o PDF
O Node.js 18 já traz fetch, então não é preciso instalar nada. Salve como gerar-pdf.mjs, porque o await no nível do arquivo exige ES modules:
import { writeFile } from 'node:fs/promises';
const apiKey = process.env.PDFSERVICES_API_KEY;
if (!apiKey) throw new Error('Defina a variável PDFSERVICES_API_KEY.');
const html = `<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<style>
@page { size: A4; margin: 2cm; }
body { font-family: sans-serif; }
</style>
</head>
<body><h1>Olá, PDF!</h1></body>
</html>`;
const response = await fetch('https://api.pdfservices.sistema.site/v1/html2pdf/html', {
method: 'POST',
headers: { 'X-Api-Key': apiKey, 'Content-Type': 'text/html; charset=utf-8' },
body: html
});
if (!response.ok) {
const erro = await response.json().catch(() => ({}));
throw new Error(`Falha ${response.status}: ${erro.error} - ${erro.message}`);
}
await writeFile('documento.pdf', Buffer.from(await response.arrayBuffer()));
console.log('Cobrado: R$', response.headers.get('x-charged-amount'));
Rode com PDFSERVICES_API_KEY=pdfs_... node gerar-pdf.mjs. Os headers da resposta trazem o valor cobrado (X-Charged-Amount), o saldo restante (X-Balance) e o identificador da transação (X-Transaction-Id).
Nunca chame a API a partir do navegador com a sua chave: ela ficaria visível para qualquer visitante. Faça a chamada no back-end e entregue apenas o PDF ao usuário.
Exemplo com Express: gerando o PDF sob demanda
Aqui a rota monta o HTML, envia no formato JSON e repassa o PDF ao cliente em streaming com Readable.fromWeb, sem guardar o arquivo inteiro na memória:
import express from 'express';
import { Readable } from 'node:stream';
const app = express();
const API_URL = 'https://api.pdfservices.sistema.site/v1/html2pdf/json';
// Todo dado vindo do usuário deve ser escapado antes de entrar no HTML.
const escapeHtml = (texto) => String(texto).replace(/[&<>"']/g, (c) => (
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
));
app.get('/faturas/:numero/pdf', async (req, res, next) => {
try {
const html = `<!doctype html>
<html lang="pt-BR"><head><meta charset="utf-8"></head>
<body><h1>Fatura ${escapeHtml(req.params.numero)}</h1></body></html>`;
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'X-Api-Key': process.env.PDFSERVICES_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ html })
});
if (!response.ok) {
return res.status(502).json({ erro: `Falha ao gerar o PDF (${response.status}).` });
}
res.set('Content-Type', 'application/pdf');
res.set('Content-Disposition', 'inline; filename="fatura.pdf"');
// Repassa o PDF em streaming, sem guardá-lo inteiro na memória.
Readable.fromWeb(response.body).pipe(res);
} catch (error) {
next(error);
}
});
app.listen(3000);
Em projetos maiores, gere o HTML com a engine de templates que você já usa. Deixe o escape automático ligado: é ele que impede que um dado do usuário vire HTML dentro do documento.
CSS para PDF: tamanho da página, cabeçalho, rodapé e quebras
O motor de conversão entende CSS de impressão (paged media). É com ele que você controla o documento, sem nada específico de Node.js:
@page {
size: A4;
margin: 2.5cm 2cm;
@top-center { content: "Relatório mensal"; }
@bottom-right { content: "Página " counter(page) " de " counter(pages); }
}
.nova-pagina { page-break-before: always; }
/* Repete o cabeçalho da tabela em todas as páginas */
thead { display: table-header-group; }
- Tamanho e margens:
@page { size: A4; },A4 landscape,Letterou medidas como21cm 29.7cm. - Cabeçalho e rodapé: as caixas de margem (
@top-center,@bottom-rightetc.) comcounter(page)ecounter(pages)geram o "Página 1 de 3". - Quebras:
page-break-before,page-break-afterepage-break-inside: avoidpara não cortar blocos ao meio. - Imagens: embutidas como
data:URI ou por URL https pública, em JPG ou PNG. - Fontes: as padrão já cobrem acentos; para a sua fonte, use
@font-facecomdata:URI. Fontes por URL, como as do Google Fonts, não são baixadas. - JavaScript não é executado: monte o HTML final no servidor. Para gráficos, gere uma imagem PNG antes.
Tratamento de erros
Quando algo dá errado, a API responde com um JSON {"error": "...", "message": "..."}. Os casos que valem tratar no código:
| HTTP | Código | O que fazer |
|---|---|---|
| 401 | invalid_api_key | Confira a chave configurada; ela pode ter sido excluída no painel. |
| 402 | insufficient_balance | Saldo insuficiente: adicione créditos. Não adianta repetir a chamada. |
| 413 | payload_too_large | O corpo passou de 1 MB. Reduza imagens embutidas ou use URLs https. |
| 422 | conversion_failed | O HTML não pôde ser convertido. Revise o documento; a falha não é cobrada. |
| 503 | busy | Muitas chamadas simultâneas na mesma conta: repita com espera crescente. |
Para novas tentativas, um laço simples com espera crescente (1 s, 2 s, 4 s) resolve. Aplique-o só ao status 503 e a falhas de rede: repetir 401, 402 ou 422 não muda o resultado.
Serverless e containers
Em AWS Lambda, Vercel, Netlify Functions ou Google Cloud Functions, rodar Puppeteer exige pacotes especiais de Chromium, mais memória e cold starts mais longos. Com uma API, a função só faz uma requisição HTTP. O pacote de deploy continua pequeno e o consumo de memória não depende do tamanho do documento.
Custos e privacidade
O PDF Services cobra R$ 0,001 por conversão bem-sucedida, com créditos pré-pagos e sem mensalidade: mil PDFs custam R$ 1,00. Conversões com erro não são cobradas.
O HTML enviado e o PDF gerado são processados só em memória e nunca são armazenados nem registrados em logs. Ficam registrados apenas metadados da transação, como data, tamanho e valor cobrado. Detalhes na Política de Privacidade. Todo o código do serviço é aberto, sob a licença AGPL v3, e pode ser auditado no GitHub.
Gere seu primeiro PDF agora
Crie a conta da sua empresa, gere uma chave no painel e rode o exemplo acima. Pelo painel também dá para testar a conversão antes de escrever uma linha de código.