ITSkillsCenter
Intelligence Artificielle

Chatbot en streaming avec Gemini, Node.js et Express

13 دقائق للقراءة

📍 Guide principal : Développer avec l’API Google Gemini — ce tutoriel assemble plusieurs de ses briques côté web.

Un assistant en console, c’est parfait pour apprendre ; mais vos utilisateurs, eux, attendent une page web où la réponse s’écrit au fil de l’eau, comme dans les interfaces de chat modernes. Ce tutoriel construit exactement cela : un petit serveur Node.js avec Express qui relaie les questions vers Gemini en streaming, et une page web qui affiche la réponse mot à mot. Au passage, on respecte la règle d’or vue dans le guide principal — la clé API ne quitte jamais le serveur. C’est le projet qui transforme tout ce que vous avez appris en une application que l’on peut réellement montrer.

Ce que vous allez apprendre

  • Initialiser un projet Node.js et installer le SDK @google/genai.
  • Générer du texte et le diffuser en streaming en JavaScript.
  • Monter un serveur Express qui expose un point d’entrée de chat.
  • Gérer l’historique d’une conversation côté serveur.
  • Consommer un flux depuis le navigateur pour un affichage progressif.
  • Garder la clé API hors de portée du client.

Ce que vous allez construire

Une application web de chat complète mais minimale : une page unique avec un champ de saisie, un serveur Express qui reçoit le message, interroge Gemini en streaming, et renvoie la réponse au navigateur au fur et à mesure. Le tout en une centaine de lignes, prêt à enrichir.

Prérequis

  • Node.js 20 ou plus récent (node --version pour vérifier) — le SDK l’exige.
  • Une clé API Gemini, comme dans le premier tutoriel.
  • Des notions de JavaScript et de la ligne de commande.
  • ⏱️ Temps estimé : environ 35 minutes.

Étape 1 — Initialiser le projet

On crée un dossier, on initialise un projet Node, et on installe les trois dépendances : le SDK Gemini, Express pour le serveur, et dotenv pour lire la clé depuis un fichier .env.

mkdir chatbot-gemini
cd chatbot-gemini
npm init -y
npm install @google/genai express dotenv

Le SDK JavaScript s’utilise avec la syntaxe des modules ES (les import). Ouvrez le fichier package.json généré et ajoutez-y la ligne "type": "module", qui active cette syntaxe. Créez ensuite un fichier .env avec votre clé, et un .gitignore contenant .env et node_modules/.

GEMINI_API_KEY=collez_votre_cle_ici

Point d’étapepackage.json contient "type": "module" et les trois dépendances apparaissent sous dependencies.

Étape 2 — Premier appel en JavaScript

Avant le serveur, vérifions que l’API répond depuis Node. Le SDK JavaScript suit la même logique qu’en Python : un client, un appel, une réponse. Créez test.js :

import 'dotenv/config';
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

const response = await ai.models.generateContent({
  model: 'gemini-2.5-flash',
  contents: 'Dis bonjour en une phrase.',
});

console.log(response.text);

Exécutez avec node test.js. La ligne import 'dotenv/config' charge la clé depuis .env ; on crée le client avec new GoogleGenAI, puis ai.models.generateContent renvoie un objet dont response.text contient la réponse. Si une phrase de salutation s’affiche, l’authentification et le SDK fonctionnent.

Point d’étape — Une phrase s’affiche dans le terminal. Une erreur d’import signale souvent l’oubli de "type": "module" dans package.json.

Étape 3 — Diffuser la réponse en streaming

Pour une interface vivante, on veut le streaming : la réponse arrive par fragments. La méthode change de generateContent à generateContentStream, et on parcourt le flux avec une boucle for await.

import 'dotenv/config';
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

const flux = await ai.models.generateContentStream({
  model: 'gemini-2.5-flash',
  contents: 'Explique en un paragraphe ce qu\'est un serveur web.',
});

for await (const morceau of flux) {
  process.stdout.write(morceau.text);
}
console.log();

Chaque morceau du flux porte un bout de texte dans morceau.text ; process.stdout.write l’affiche sans saut de ligne, si bien que le texte se construit progressivement à l’écran. C’est ce flux que l’on va, à l’étape suivante, rediriger vers le navigateur au lieu du terminal.

Point d’étape — Le texte apparaît graduellement. Sinon, vérifiez l’appel à generateContentStream et la boucle for await.

Le streaming, sous le capot

Pourquoi le streaming donne-t-il une telle impression de réactivité ? La différence n’est pas dans le temps total — une réponse complète met le même temps à être générée — mais dans le moment où l’utilisateur voit quelque chose. Sans streaming, le serveur attend que Gemini ait fini de produire toute la réponse, puis l’envoie d’un bloc : l’utilisateur fixe un écran figé pendant plusieurs secondes. Avec le streaming, dès que le modèle a produit ses premiers mots, ils partent vers le navigateur. Le temps avant le premier caractère affiché chute de quelques secondes à quelques centaines de millisecondes. Techniquement, le serveur garde la connexion HTTP ouverte et écrit des fragments au fil de l’eau, tandis que le navigateur lit ce flux morceau par morceau. Cette perception de vitesse compte énormément sur une connexion à débit modeste, où chaque seconde d’attente perçue pèse sur l’expérience.

Étape 4 — Monter le serveur Express

On construit maintenant le serveur. Il expose un point d’entrée /chat qui reçoit un message, interroge Gemini en streaming, et écrit chaque fragment dans la réponse HTTP au fur et à mesure. Créez serveur.js :

import 'dotenv/config';
import express from 'express';
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const app = express();
app.use(express.json());
app.use(express.static('public'));

app.post('/chat', async (req, res) => {
  const historique = req.body.historique || [];
  historique.push({ role: 'user', parts: [{ text: req.body.message }] });

  res.setHeader('Content-Type', 'text/plain; charset=utf-8');

  const flux = await ai.models.generateContentStream({
    model: 'gemini-2.5-flash',
    contents: historique,
  });

  for await (const morceau of flux) {
    if (morceau.text) res.write(morceau.text);
  }
  res.end();
});

app.listen(3000, () => console.log('Serveur sur http://localhost:3000'));

Décortiquons les pièces. express.json() permet de lire le corps JSON des requêtes ; express.static('public') servira notre page web. Le point d’entrée /chat récupère le message, lance le streaming vers Gemini, et relaie chaque fragment au client via res.write. On ne ferme la réponse avec res.end() qu’une fois le flux épuisé. Lancez avec node serveur.js : le message de démarrage doit s’afficher.

Point d’étape — Le terminal affiche « Serveur sur http://localhost:3000 ». Le serveur attend, c’est normal : il nous manque la page web.

Étape 5 — Comprendre l’historique

Vous avez peut-être remarqué le tableau historique. C’est lui qui donne de la mémoire à la conversation. Plutôt que d’envoyer une simple chaîne, on envoie une liste de tours, chacun avec un rôleuser pour l’utilisateur, model pour les réponses passées — et son texte. Le navigateur renverra cet historique à chaque message, et le serveur y ajoutera le nouveau tour de l’utilisateur avant l’appel.

Côté client, après chaque réponse complète, on ajoutera donc deux entrées à l’historique : le message de l’utilisateur et la réponse du modèle. Le serveur reste ainsi sans état, ce qui le simplifie : toute la mémoire vit dans l’historique transporté par les requêtes. Pour une vraie application multi-utilisateurs, on rangerait plutôt cet historique dans une session côté serveur, mais le principe des rôles reste le même.

Un point à garder à l’esprit pour la suite : comme tout l’historique repart au modèle à chaque message, une conversation qui s’allonge coûte de plus en plus cher en jetons. Pour un assistant à échanges courts, c’est négligeable ; pour un service qui tient de longs dialogues, on apprend vite à tronquer les tours les plus anciens, ou à les résumer périodiquement, afin de garder le contexte utile sans laisser la facture enfler. Ce n’est pas à gérer dès la première version, mais c’est un réflexe à acquérir avant la mise en production.

Étape 6 — La page web qui consomme le flux

Dernière pièce : l’interface. Créez un dossier public et, dedans, un fichier index.html. Il contient un champ de saisie et un script qui envoie le message au serveur, puis lit la réponse en streaming pour l’afficher au fur et à mesure.

<!DOCTYPE html>
<html lang="fr">
<head><meta charset="utf-8"><title>Chat Gemini</title></head>
<body>
  <div id="conversation"></div>
  <input id="saisie" placeholder="Votre message">
  <button id="envoyer">Envoyer</button>

  <script>
    const historique = [];

    document.getElementById('envoyer').onclick = async () => {
      const champ = document.getElementById('saisie');
      const message = champ.value;
      champ.value = '';

      const conv = document.getElementById('conversation');
      conv.innerHTML += '<p><b>Vous :</b> ' + message + '</p>';
      const bulle = document.createElement('p');
      bulle.innerHTML = '<b>Assistant :</b> ';
      conv.appendChild(bulle);

      const res = await fetch('/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: message, historique: historique }),
      });

      const lecteur = res.body.getReader();
      const decodeur = new TextDecoder();
      let complet = '';
      while (true) {
        const lot = await lecteur.read();
        if (lot.done) break;
        const texte = decodeur.decode(lot.value, { stream: true });
        complet += texte;
        bulle.innerHTML += texte;
      }

      historique.push({ role: 'user', parts: [{ text: message }] });
      historique.push({ role: 'model', parts: [{ text: complet }] });
    };
  </script>
</body>
</html>

Le script fait trois choses. Il envoie le message et l’historique au serveur en JSON. Il lit ensuite la réponse en streaming grâce à un reader sur le corps de la réponse : chaque lot de données est décodé et ajouté à la bulle de l’assistant, qui se remplit sous vos yeux. Enfin, une fois la réponse complète, il met à jour l’historique avec les deux nouveaux tours, pour que le message suivant garde le contexte. Rechargez http://localhost:3000 et discutez : la réponse s’écrit progressivement, et l’assistant se souvient de ce que vous avez dit.

Point d’étape — La réponse s’affiche mot à mot, et une question de suivi (« et en moins cher ? ») tient compte du message précédent.

Étape 7 — La clé reste côté serveur

Observez bien l’architecture : le navigateur n’a jamais vu la clé API. Il parle à votre serveur, et c’est le serveur, seul détenteur de la clé, qui parle à Gemini. C’est la règle absolue rappelée dans le guide principal. Si vous aviez appelé Gemini directement depuis le JavaScript de la page, n’importe quel visiteur aurait pu lire votre clé dans le code source et l’utiliser à vos frais. Le serveur intermédiaire n’est pas une complication : c’est la condition d’une application sûre.

Point d’étape — Cherchez votre clé dans le code de la page (clic droit, code source) : elle ne doit apparaître nulle part.

Étape 8 — Penser au déploiement

Sur votre machine, le serveur tourne tant que le terminal reste ouvert. En production, on veut qu’il survive aux fermetures de session et redémarre s’il plante. Trois réflexes suffisent pour démarrer. D’abord, on confie le processus à un gestionnaire comme pm2, qui relance automatiquement le serveur en cas d’arrêt. Ensuite, la clé API ne vit plus dans un fichier .env déposé sur le serveur, mais dans les variables d’environnement de l’hébergeur, plus sûres. Enfin, on place le serveur derrière un reverse proxy qui gère le HTTPS, condition indispensable pour une application publique. Un petit serveur de ce type tient sans peine sur l’offre la moins chère d’un hébergeur à quelques milliers de francs par mois, car il ne fait que relayer des requêtes — le travail lourd se passe chez Gemini.

Point d’étape — Vous savez nommer les trois éléments d’une mise en production : gestionnaire de processus, secrets côté hôte, HTTPS.

🐞 Pièges fréquents

Symptôme Cause probable Correctif
Cannot use import statement "type": "module" absent de package.json. L’ajouter, puis relancer.
Clé non lue import 'dotenv/config' manquant ou .env mal formé. Importer dotenv en première ligne ; vérifier le .env.
La page ne se charge pas Dossier public absent ou express.static oublié. Créer public/index.html et servir le dossier.
Réponse affichée d’un bloc Lecture de la réponse en une fois au lieu du flux. Utiliser le reader et la boucle de lecture.
L’assistant n’a pas de mémoire Historique non renvoyé ou non mis à jour côté client. Pousser les tours user et model après chaque réponse.

✅ Récapitulatif

Vous avez assemblé une vraie application de chat. Vous savez initialiser un projet Node en modules ES, appeler Gemini en JavaScript, diffuser une réponse en streaming, monter un serveur Express qui relaie ce flux, gérer l’historique par rôles pour la mémoire de conversation, et consommer le flux côté navigateur pour un affichage progressif — le tout en gardant la clé hors de portée du client. C’est le squelette sur lequel se construisent la plupart des assistants web : il ne reste qu’à brancher dessus les autres briques de cette série, comme le RAG pour répondre sur vos données.

🧾 Aide-mémoire

Élément Rôle
npm install @google/genai express dotenv Installer les dépendances.
new GoogleGenAI({ apiKey }) Créer le client.
ai.models.generateContentStream({ model, contents }) Générer en streaming.
for await (const morceau of flux) Parcourir les fragments.
res.write(...) / res.end() Relayer le flux au client.
{ role, parts: [{ text }] } Un tour de conversation.

💪 À vous de jouer

Donnez une personnalité à l’assistant via une instruction système, et ajoutez un bouton « Effacer » qui vide l’historique pour repartir d’une conversation vierge.

Voir une solution
// Cote serveur, passer une instruction systeme dans la configuration :
const flux = await ai.models.generateContentStream({
  model: 'gemini-2.5-flash',
  contents: historique,
  config: {
    systemInstruction: 'Tu es un assistant commercial poli, en francais, concis.',
  },
});

// Cote client, le bouton Effacer :
document.getElementById('effacer').onclick = () => {
  historique.length = 0;
  document.getElementById('conversation').innerHTML = '';
};

L’instruction système se passe dans config.systemInstruction côté serveur, et vider historique côté client suffit à réinitialiser la mémoire.

Tutoriels suivants

Pour aller plus loin

🔝 Retour au guide principal de l’API Gemini. La documentation officielle du SDK @google/genai détaille les options de streaming et de configuration.

Questions fréquentes

Pourquoi un serveur, et pas un appel direct depuis la page ? Parce que la clé API serait alors visible de tous. Le serveur la garde secrète et sert d’intermédiaire.

Le serveur garde-t-il l’historique ? Dans cette version, non : l’historique voyage avec les requêtes. Pour une application réelle, on le stockerait dans une session côté serveur.

Peut-on faire la même chose avec un framework front comme React ? Oui. La logique de lecture du flux est identique ; seul l’affichage change selon le framework.

Et en Python côté serveur ? C’est possible aussi, avec un framework comme FastAPI et la version Python du SDK ; le principe du relais de flux est le même.

مشاركة
Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité