Générer des rapports HTML avec EJS ou Pug dans Node.js
La génération de rapports HTML à partir de données applicatives répond à de nombreux besoins : factures, tableaux de bord, relevés d’activité, comptes rendus d’exécution ou pages imprimables. Dans un projet Node.js, un moteur de templates permet de séparer les données, la structure visuelle et la logique de génération.
EJS et Pug sont deux solutions populaires pour produire des documents HTML dynamiques. Leur approche diffère toutefois sur la syntaxe, la lisibilité, la gestion des composants et le niveau de contrôle laissé au développeur. Un choix adapté facilite la maintenance et rend la production de rapports plus fiable.
Choisir Le Moteur Adapté
EJS repose sur une syntaxe HTML enrichie par des balises JavaScript. Cette proximité avec le HTML facilite l’adoption par une équipe habituée aux interfaces web classiques. Les expressions s’insèrent avec <%= valeur %>, tandis que les blocs de logique utilisent <% ... %>.
Pug, anciennement appelé Jade, propose une syntaxe basée sur l’indentation. Les balises fermantes et une grande partie des caractères répétitifs disparaissent, ce qui rend les fichiers compacts. En contrepartie, une indentation incorrecte peut modifier la structure produite et compliquer le diagnostic d’une erreur de rendu.
Le choix dépend surtout du contexte. EJS convient bien à un rapport dont la mise en page contient beaucoup de HTML existant. Pug devient intéressant pour standardiser des vues courtes, factorisées et fortement composées.
Structurer Les Données Du Rapport
Un moteur de templates ne devrait pas récupérer directement des données depuis plusieurs sources. La préparation doit être réalisée dans une couche dédiée : requêtes vers la base, calculs, regroupements et formatage des valeurs. Le template reçoit ensuite un objet cohérent, prêt à être affiché.
Par exemple, une fonction peut construire un modèle de rapport :
const rapport = {
titre: "Activité mensuelle",
periode: "Mars 2025",
lignes: commandes.map((commande) => ({
reference: commande.reference,
client: commande.client.nom,
total: `${commande.total.toFixed(2)} €`
})),
totalGeneral: total.toFixed(2)
};
Cette séparation limite la logique métier dans les fichiers de présentation. Pour approfondir l’organisation des sources et des requêtes, les ressources consacrées aux bases de données apportent un cadre utile pour choisir entre SQL, ORM et services spécialisés.
Générer Un Rapport Avec EJS
L’installation commence par l’ajout du moteur et la configuration d’Express :
npm install express ejs
import express from "express";
const app = express();
app.set("view engine", "ejs");
app.set("views", "./views");
app.get("/rapport", async (req, res) => {
const donnees = await chargerRapport();
res.render("rapport", donnees);
});
Le fichier views/rapport.ejs conserve une structure HTML familière :
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title><%= titre %></title>
</head>
<body>
<h1><%= titre %></h1>
<p>Période : <%= periode %></p>
<table>
<tbody>
<% lignes.forEach((ligne) => { %>
<tr>
<td><%= ligne.reference %></td>
<td><%= ligne.client %></td>
<td><%= ligne.total %></td>
</tr>
<% }); %>
</tbody>
</table>
<strong>Total : <%= totalGeneral %></strong>
</body>
</html>
Par défaut, <%= ... %> échappe les caractères HTML. Cette protection est précieuse pour empêcher l’injection de contenu fourni par un utilisateur. La syntaxe <%- ... %>, qui désactive l’échappement, doit rester limitée à du HTML maîtrisé.
Construire Une Vue Avec Pug
Pug s’installe de manière comparable :
npm install express pug
La configuration Express devient presque identique :
app.set("view engine", "pug");
app.set("views", "./views");
Un template Pug correspondant pourrait être écrit ainsi :
doctype html
html(lang="fr")
head
meta(charset="utf-8")
title= titre
body
h1= titre
p Période : #{periode}
table
tbody
each ligne in lignes
tr
td= ligne.reference
td= ligne.client
td= ligne.total
strong Total : #{totalGeneral}
Pug facilite la création de layouts et de blocs réutilisables. Un fichier parent peut contenir l’en-tête, les feuilles de style et le pied de page, tandis que le rapport spécialisé ne définit que son contenu. Cette organisation convient aux applications qui produisent plusieurs documents partageant la même charte graphique.
Sécuriser Le Rendu HTML
La sécurité commence par l’échappement des valeurs. Les noms de clients, commentaires, références ou libellés provenant d’une requête doivent être considérés comme non fiables. Même un rapport interne peut être consulté par plusieurs profils et contenir des données saisies depuis un formulaire.
Il faut également contrôler les URL, les chemins de fichiers et les fragments HTML transmis au template. L’utilisation d’un fragment non nettoyé peut introduire une faille XSS. Lorsque l’insertion de HTML riche est nécessaire, une bibliothèque de nettoyage doit filtrer les balises et attributs autorisés avant le rendu.
La génération sur le serveur mérite aussi une attention particulière. Les données confidentielles ne doivent pas être ajoutées au document si elles ne sont pas nécessaires. Les en-têtes HTTP, une politique de sécurité du contenu et des contrôles d’accès adaptés complètent la protection du modèle de vue.
Tester Le Contenu Produit
Un rapport valide ne se limite pas à une réponse HTTP avec un code 200. Les tests doivent vérifier la présence du titre, le nombre de lignes, le total calculé et l’échappement des caractères spéciaux. Un jeu de données vide doit également produire un document compréhensible, avec un message adapté plutôt qu’un tableau cassé.
Le rendu peut être testé avec Jest, Vitest ou Mocha en appelant directement la fonction de génération. Les différences entre ces outils sont détaillées dans ce guide consacré aux tests des projets Node.js. Pour une veille complémentaire sur les pratiques de développement, le site de Yanila Vigne constitue également une ressource pertinente.
Un test simple peut contrôler une sortie rendue :
import { renderFile } from "ejs";
test("affiche le total du rapport", async () => {
const html = await renderFile("views/rapport.ejs", {
titre: "Activité",
periode: "Mars",
lignes: [],
totalGeneral: "125.00"
});
expect(html).toContain("125.00");
expect(html).toContain("<title>Activité</title>");
});
Industrialiser La Production Des Documents
Pour un petit volume, le rendu à la demande depuis une route Express suffit. Pour des rapports lourds ou nombreux, une file de tâches permet de déplacer le traitement vers un worker. BullMQ, RabbitMQ ou un service cloud peuvent orchestrer la génération et éviter de bloquer les requêtes web.
La conversion en PDF peut ensuite être réalisée avec Playwright ou Puppeteer. Le HTML doit alors intégrer des styles d’impression : marges, sauts de page, en-têtes et pieds de page. Les ressources statiques doivent être accessibles dans l’environnement de production, y compris lorsque le navigateur s’exécute dans un conteneur Docker.
Le cache mérite une stratégie explicite. Un rapport identique peut être conservé avec une clé fondée sur l’identifiant, la période et la version du modèle. Il faut toutefois invalider ce cache lorsqu’une donnée change ou lorsqu’un template est déployé.
| Critère | EJS | Pug |
|---|---|---|
| Proximité avec le HTML | Très élevée | Moyenne |
| Courbe d’apprentissage | Rapide pour les débutants HTML | Nécessite d’apprendre la syntaxe |
| Lisibilité des vues longues | Familiarité appréciable | Structure compacte |
| Factorisation des layouts | Bonne | Très bonne |
| Risque principal | Mélange excessif avec JavaScript | Erreur d’indentation |
| Cas adapté | Rapports proches d’un HTML classique | Interfaces et documents très composés |
Pour démarrer rapidement, EJS constitue souvent le choix le plus direct. Pug devient pertinent lorsque la réduction du code répétitif et la composition des vues sont prioritaires. Dans les deux cas, la qualité du résultat dépend surtout de la préparation des données, de la sécurité de l’échappement et de tests représentatifs.
Intégrez un premier template dans votre application Node.js, ajoutez un jeu de données réaliste, puis validez le rendu dans un navigateur et dans votre pipeline de tests. Une base simple et bien séparée permettra ensuite d’ajouter les styles, l’export PDF et l’exécution asynchrone sans réécrire toute l’architecture.