Bob le développeur
Estimer

Générer des PDF avec Puppeteer dans NestJS : ce qu'on a appris en production

Bob Le Développeur

09/03/2026 · Mis à jour le 14/10/2026

On a utilisé Puppeteer pour générer des factures et des formulaires administratifs en PDF dans deux projets NestJS. C'est puissant, mais ça vient avec des pièges qu'on n'a trouvés dans aucun tutoriel.

Pourquoi Puppeteer

Le besoin : générer des PDF à partir de données dynamiques : factures avec détails de paiement, formulaires pré-remplis avec 60+ champs, rapports personnalisés. Les alternatives (jsPDF, PDFKit) construisent le PDF pixel par pixel. Puppeteer prend du HTML et le rend en PDF, comme un navigateur qui "imprime" la page. Hyper pratique.

Pour nous, c'était le choix évident : on sait écrire du HTML comme des pros.

Le service de base

@Injectable()
export class PdfService {
  async generatePdf(htmlContent: string, outputPath: string) {
    const browser = await Puppeteer.launch({
      headless: true,
      pipe: true,
      executablePath: process.env.CHROMIUM_PATH,
      args: ['--no-sandbox', '--disable-setuid-sandbox'],
    });

    const page = await browser.newPage();
    await page.setContent(htmlContent, { waitUntil: 'load' });
    await page.pdf({
      path: outputPath,
      format: 'A4',
      printBackground: true,
    });

    await browser.close();
    return outputPath;
  }
}

Simple en apparence. Les problèmes arrivent après.

Piège #1 : Docker et Chromium

Puppeteer télécharge automatiquement Chrome for Testing en local. En Docker, c'est une autre histoire. Chromium a besoin de librairies système spécifiques. Notre Dockerfile inclut :

RUN apt-get update && apt-get install -y \
  chromium \
  fonts-liberation \
  --no-install-recommends

Et dans le service, on pointe vers le Chromium système :

executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium'

Tip : ne laissez pas Puppeteer télécharger son propre Chromium en Docker. Ça ajoute ~300 Mo à l'image et ça casse régulièrement entre les versions, c'est super chiant. Utilisez le Chromium du système, et déclarez ENV PUPPETEER_SKIP_DOWNLOAD=true avant le npm install : sans cette variable, l'installation télécharge quand même le navigateur. Vous nous remercierez. Seule contrepartie : chaque version de Puppeteer est prévue pour une version précise de Chrome, et le Chromium de la distribution peut s'en écarter.

Piège #2 : les fuites mémoire

Chaque Puppeteer.launch() crée un process Chromium. Si le browser.close() n'est pas appelé (exception, timeout), le process reste en mémoire. Après quelques dizaines de PDF, le serveur est à genoux. C'est vraiment pas joli à voir, croyez-nous.

Notre pattern : toujours fermer dans un finally.

let browser: Browser | undefined;
try {
  browser = await Puppeteer.launch(/* ... */);
  // ... générer le PDF
} finally {
  if (browser) await browser.close();
}

Tip avancé : pour du volume (plus de 10 PDF/minute), gardez une instance de browser ouverte et créez une nouvelle page à chaque PDF au lieu de relancer Chromium à chaque fois. On n'a pas eu besoin d'aller jusque-là, mais c'est le pattern recommandé pour la performance.

Piège #3 : waitUntil et les ressources externes

L'option waitUntil: 'load' attend l'événement load de la page, donc le chargement des images, feuilles de style et polices qu'elle référence (setContent n'accepte plus networkidle0 depuis Puppeteer 24.43.1). Si votre HTML charge des images externes ou des fonts Google, Puppeteer attend qu'elles soient toutes chargées avant de générer le PDF.

En pratique, une image lente ralentit d'autant la génération, et une ressource qui ne répond jamais la bloque jusqu'au délai de 30 secondes, après quoi setContent lève une erreur. Une image en 404, elle, ne bloque rien.

Tip : embarquez tout en inline : images en base64, CSS inline, pas de fonts externes. Le chargement du HTML devient quasi instantané ; le lancement de Chromium à chaque PDF reste alors le poste le plus lent.

Piège #4 : les dimensions

Sans unité, width et height sont lus par Puppeteer comme des pixels CSS (96 par pouce), pas comme des points : width: 595, height: 842 donne une page d'environ 157 × 223 mm, pas un A4. Pour un A4, utilisez format: 'A4', ou des valeurs avec unité (width: '210mm', height: '297mm').

Pour les marges, on les gère directement en CSS dans le HTML plutôt qu'avec l'option margin de Puppeteer, plus de contrôle, et c'est plus facile à débugger en ouvrant le HTML dans un navigateur.

L'alternative : EJS pour le templating

Sur un autre projet, on utilise des templates EJS pour séparer la structure HTML des données :

template.ejs → inject données → HTML complet → Puppeteer → PDF

C'est plus maintenable que de construire le HTML dans le code TypeScript, surtout pour des documents complexes (factures avec lignes variables, en-têtes conditionnels).

Quand NE PAS utiliser Puppeteer

  • PDF simple (texte + tableau) : PDFKit ou jsPDF sont plus légers et ne nécessitent pas Chromium
  • Remplissage de formulaire PDF existant : pdf-lib manipule des PDF existants, parfait pour pré-remplir un formulaire officiel
  • Très haut volume : si vous générez des centaines de PDF par minute, un service dédié (ou un worker avec browser pool) vaut mieux qu'une instance Puppeteer dans votre API

On a d'ailleurs combiné les deux sur un projet : Puppeteer pour les factures (layout HTML complexe) et pdf-lib pour pré-remplir un formulaire ACRE officiel (60+ champs dans un PDF existant).

En contexte : on a utilisé Puppeteer pour les factures d'une marketplace de santé et pdf-lib pour les formulaires ACRE d'une plateforme de création d'entreprise.

Stack concernée : NestJS, Puppeteer, Docker, EJS, pdf-lib

Ce contenu vous a servi ? Ajoutez Bob le développeur à vos sources préférées sur Google.

Renfort

Besoin de renfort dans votre équipe ?

Des développeurs React, Next.js et Node.js seniors, intégrés à vos sprints et à vos revues de code.

  • Démarrage en quelques jours
  • Engagement au mois

30 min, gratuit, sans engagement.

La newsletter qu’on n’ignore pas

Abonnez-vous à notre newsletter pour recevoir nos derniers articles, retours d’expérience et conseils tech directement dans votre boîte mail.

Désinscription en un clic. Vos données restent privées.