Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 915 minIntermédiaire

Node.js et MongoDB : le driver officiel

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • te connecter une seule fois à MongoDB depuis Node.js ;
  • écrire les opérations CRUD avec le driver officiel ;
  • convertir et comparer correctement des ObjectId ;
  • choisir entre le driver officiel et Mongoose.

2 exercices corrigés

Le driver officiel MongoDB pour Node.js est la bibliothèque mongodb, publiée par MongoDB Inc., qui permet à un programme JavaScript de se connecter à une base et d'y exécuter les mêmes opérations que dans mongosh : find, insertOne, updateOne, aggregate… Toutes ses méthodes sont asynchrones et s'utilisent avec await. Ce chapitre montre comment se connecter une seule fois au démarrage, comment écrire les requêtes depuis Node.js et quels pièges distinguent le code du shell. Il suppose que tu connais Node.js, les modules ES et async/await, vus dans le cours Node.js et Express (npm, package.json et modules ES).

Installer et se connecter

bash
npm install mongodb      # version 7 en 2026, qui demande Node.js 20.19 ou plus récent
js
// src/db.js
import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URI);

export async function connecter() {
  await client.connect();
  return client.db();          // la base indiquée dans l'URI (appli_frais)
}

export async function deconnecter() {
  await client.close();
}
js
// essai.js : node --env-file=.env essai.js
import { connecter, deconnecter } from './src/db.js';

const db = await connecter();
const fiches = db.collection('fiches');
console.log(await fiches.countDocuments({ etat: 'CR' }));   // 3
await deconnecter();

node --env-file=.env charge les variables du fichier .env dans process.env, sans bibliothèque supplémentaire, depuis Node.js 20.6.

Si le serveur est injoignable, connect() attend 30 secondes par défaut, puis échoue avec une MongoServerSelectionError (connect ECONNREFUSED 127.0.0.1:27017 : rien n'écoute sur ce port, le conteneur est sans doute arrêté). L'option serverSelectionTimeoutMS raccourcit cette attente pendant le développement.

Le CRUD depuis Node.js

Les méthodes sont celles du shell, avec await devant :

js
const fiches = db.collection('fiches');

// Lire : find renvoie un curseur, toArray le transforme en tableau
const enCours = await fiches
  .find({ etat: 'CR' }, { projection: { _id: 0, mois: 1, visiteur: 1 } })
  .sort({ mois: 1 })
  .toArray();

// Un seul document, ou null
const lucas = await db.collection('visiteurs')
  .findOne({ matricule: 'a131' }, { projection: { nom: 1, prenom: 1 } });
// { _id: new ObjectId('6a0000000000000000000001'), nom: 'Martin', prenom: 'Lucas' }

// Créer
const { insertedId } = await fiches.insertOne({
  visiteur: lucas._id, mois: '2026-11', etat: 'CR',
  lignesForfait: [], fraisHorsForfait: [], montantValide: null, dateModif: new Date()
});

// Modifier
const resultat = await fiches.updateOne(
  { _id: insertedId },
  { $push: { lignesForfait: { code: 'REP', quantite: 1, montantUnitaire: 25 } },
    $currentDate: { dateModif: true } }
);
// { acknowledged: true, modifiedCount: 1, upsertedId: null, upsertedCount: 0, matchedCount: 1 }

// Modifier et récupérer le document modifié
const cloturee = await fiches.findOneAndUpdate(
  { _id: insertedId, etat: 'CR' },
  { $set: { etat: 'CL' } },
  { returnDocument: 'after', projection: { etat: 1, lignesForfait: 1 } }
);
// { _id: …, etat: 'CL', lignesForfait: [ { code: 'REP', quantite: 1, montantUnitaire: 25 } ] }
// ou null si aucune fiche en cours ne correspondait

// Supprimer
await fiches.deleteOne({ _id: insertedId });   // { acknowledged: true, deletedCount: 1 }

// Agréger : même pipeline que dans mongosh
const parEtat = await fiches.aggregate([
  { $group: { _id: '$etat', nb: { $sum: 1 } } },
  { $sort: { _id: 1 } }
]).toArray();

Pour parcourir un très grand résultat sans tout charger en mémoire, itère sur le curseur au lieu d'appeler toArray() : il récupère les documents par lots.

js
for await (const fiche of fiches.find({ etat: 'RB' })) {
  // traiter une fiche à la fois
}

Les pièges du passage de mongosh à Node.js

Les identifiants arrivent en chaînes. Dans une API, l'identifiant d'une fiche vient de l'URL (/v1/fiches/6ae8…) : c'est une chaîne. Le driver ne la convertit pas : find({ _id: '6ae8…' }) ne trouve rien. Il faut construire un ObjectId, après avoir vérifié la chaîne, car le constructeur lève une exception sur une valeur mal formée :

js
import { ObjectId } from 'mongodb';

ObjectId.isValid('6a0000000000000000000001');   // true
ObjectId.isValid('123');                        // false
new ObjectId('123');
// BSONError: input must be a 24 character hex string, 12 byte Uint8Array, or an integer

export async function trouverFiche(db, id) {
  if (!ObjectId.isValid(id)) return null;        // l'API répondra 404 ou 400
  return db.collection('fiches').findOne({ _id: new ObjectId(id) });
}

Comparer des ObjectId. Deux ObjectId sont des objets : === compare leurs adresses en mémoire, jamais leur valeur, et un ObjectId n'est jamais === à une chaîne. Utilise la méthode equals, qui accepte un ObjectId ou une chaîne :

js
fiche.visiteur === '6a0000000000000000000001'         // false, toujours
fiche.visiteur.equals('6a0000000000000000000001')     // true
fiche.visiteur.toString()                             // '6a0000000000000000000001'

C'est exactement la faille décrite dans le cours de sécurité : un contrôle de propriétaire écrit avec !== refuse tout le monde, ou, écrit à l'envers, laisse passer tout le monde.

La sérialisation en JSON. res.json(fiche) transforme les ObjectId en chaînes ("6a0000000000000000000001") et les dates en chaînes ISO 8601 : le client de ton API reçoit du JSON standard. Dans l'autre sens, rien n'est automatique : une date reçue en JSON est une chaîne, à convertir avec new Date(…) avant de l'enregistrer.

Les erreurs. Une violation d'index unique lève une MongoServerError dont le code vaut 11000 ; une violation du validateur $jsonSchema, le code 121. Teste le code, pas le texte du message :

js
try {
  await fiches.insertOne(nouvelleFiche);
} catch (err) {
  if (err.code === 11000) {
    // une fiche existe déjà pour ce visiteur et ce mois : l'API répondra 409
  } else {
    throw err;
  }
}

Ranger l'accès aux données dans un module

Le code de l'API ne doit pas appeler db.collection('fiches') partout. Regroupe les requêtes d'une collection dans un module d'accès aux données (un repository, ou la couche modèle d'une architecture MVC, voir le chapitre Repository et DAO du cours Architecture MVC) :

js
// src/repositories/fiches.repository.js
import { ObjectId } from 'mongodb';

export function fichesRepository(db) {
  const fiches = db.collection('fiches');
  return {
    parVisiteur: (idVisiteur) =>
      fiches.find({ visiteur: new ObjectId(idVisiteur) }).sort({ mois: -1 }).toArray(),

    duMois: (idVisiteur, mois) =>
      fiches.findOne({ visiteur: new ObjectId(idVisiteur), mois }),

    creer: async (idVisiteur, mois) => {
      const fiche = { visiteur: new ObjectId(idVisiteur), mois, etat: 'CR',
                      lignesForfait: [], fraisHorsForfait: [], montantValide: null, dateModif: new Date() };
      const { insertedId } = await fiches.insertOne(fiche);
      return { _id: insertedId, ...fiche };
    },

    cloturer: (id, idVisiteur) =>
      fiches.findOneAndUpdate(
        { _id: new ObjectId(id), visiteur: new ObjectId(idVisiteur), etat: 'CR' },
        { $set: { etat: 'CL' }, $currentDate: { dateModif: true } },
        { returnDocument: 'after' }
      ),
  };
}

Les contrôleurs appellent repo.cloturer(id, req.utilisateur.id) sans connaître MongoDB. Si un jour la base change, seul ce module change. Les identifiants reçus doivent être validés avant d'arriver ici (le chapitre 12 le fait avec Zod), sinon new ObjectId lève une exception.

Driver officiel ou Mongoose ?

Driver mongodbMongoose
Ce que c'estLa bibliothèque de base, maintenue par MongoDB Inc.Une surcouche (un ODM, Object Document Mapper) construite sur le driver
SchémaAucun côté application (validateur $jsonSchema côté base possible)Schémas avec types, valeurs par défaut, validation, champs calculés
Conversion des typesÀ faire soi-même (new ObjectId, new Date)Automatique d'après le schéma
Relations$lookuppopulate, en plus de $lookup
HooksNonMiddleware pre et post (avant l'enregistrement, après la suppression…)
PerformanceLe plus directUn léger surcoût, que lean() réduit
Quand le choisirScripts, outils, données de forme très variable, besoin de toutes les fonctions récentesUne API métier avec des entités bien définies : le cas de la plupart des projets de BTS

Les deux se combinent : Mongoose expose le driver (Fiche.collection) pour les rares opérations qu'il ne couvre pas. Le chapitre suivant reprend AppliFrais avec Mongoose, que l'API du chapitre 12 utilise.

Exercices

Exercice 1 sur 2 Trouver les erreurs Réussi À revoir

Ce contrôleur d'une API Express renvoie toujours 404, et le serveur finit par refuser les connexions. Trouve les trois erreurs.

js
import { MongoClient } from 'mongodb';

export async function detailFiche(req, res) {
  const client = new MongoClient(process.env.MONGODB_URI);
  await client.connect();
  const fiche = await client.db().collection('fiches')
    .findOne({ _id: req.params.id }, { mois: 1, etat: 1, montantValide: 1 });
  if (!fiche) return res.status(404).json({ title: 'Fiche introuvable' });
  res.json(fiche);
}
Voir la réponseMasquer la réponse de l'exercice 1
  1. Un client par requête, jamais fermé : chaque appel ouvre un nouveau pool de connexions, et aucun n'est refermé (client.close() n'est jamais appelé). Le serveur atteint sa limite de connexions. Il faut un client unique, créé au démarrage de l'application, et le réutiliser.
  2. L'identifiant reste une chaîne : { _id: req.params.id } ne correspond jamais à un ObjectId, d'où le 404 systématique. Il faut ObjectId.isValid(req.params.id), puis { _id: new ObjectId(req.params.id) }.
  3. La projection est mal placée : le deuxième argument est l'objet d'options ; il faut { projection: { mois: 1, etat: 1, montantValide: 1 } }. En l'état, le document complet serait renvoyé.

Il manque aussi le contrôle d'accès : n'importe quel utilisateur connecté lirait la fiche de n'importe qui (chapitre 13).

Tu avais trouvé ?

Exercice 2 sur 2 Écrire un module d'accès aux données Réussi À revoir

Complète le module fichesRepository avec deux fonctions :

  1. ajouterHorsForfait(id, idVisiteur, frais) ajoute un frais hors forfait ({ date, libelle, montant }) à une fiche, seulement si elle appartient au visiteur et qu'elle est en cours ; elle renvoie true si la fiche a été modifiée.
  2. totauxParEtat() renvoie le nombre de fiches et la somme des montants validés pour chaque état.
Voir la réponseMasquer la réponse de l'exercice 2
js
ajouterHorsForfait: async (id, idVisiteur, { date, libelle, montant }) => {
  const { modifiedCount } = await fiches.updateOne(
    { _id: new ObjectId(id), visiteur: new ObjectId(idVisiteur), etat: 'CR' },
    { $push: { fraisHorsForfait: { _id: new ObjectId(), date: new Date(date), libelle, montant } },
      $currentDate: { dateModif: true } }
  );
  return modifiedCount === 1;
},

totauxParEtat: () =>
  fiches.aggregate([
    { $group: { _id: '$etat', nb: { $sum: 1 }, total: { $sum: '$montantValide' } } },
    { $sort: { _id: 1 } }
  ]).toArray(),

Les conditions de propriété et d'état sont dans le filtre : si la fiche n'appartient pas au visiteur ou n'est plus en cours, rien n'est modifié et la fonction renvoie false. On ne passe pas l'objet frais reçu tel quel : on en extrait les trois champs attendus, et la date est convertie. Donner un _id à chaque frais hors forfait permettra de le désigner pour le supprimer ($pull: { fraisHorsForfait: { _id: … } }) ; Mongoose le fait automatiquement pour les sous-documents.

Tu avais trouvé ?

Chapitre terminé !

Fais les 2 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Mongoose : schéma, validation, middleware et populateRevoir le sommaire du cours