Qu’est-ce que MongoDB ?
MongoDB s’inscrit dans la lignée des bases de données (BD) NoSQL, et plus précisément
des bases de données orientées documents.
Là où une BD relationnelle organise les données dans des tables, composées de lignes
et de colonnes suivant un schéma défini, MongoDB stocke les données sous forme de documents
(structures proches d’objets JSON) regroupés dans des collections.
Par exemple, une collection etudiants pourrait contenir les deux documents suivants :
Étudiant 1
{
"_id": "1",
"nom": "Amina",
"programme": "Informatique",
"cours": ["IFT1015", "IFT2255"]
}
Étudiant 2
{
"_id": "2",
"nom": "Thomas",
"programme": "Mathématiques",
"courriel": "thomas@example.com"
}
Ces deux documents appartiennent à la même collection, mais ils ne possèdent pas
exactement les mêmes champs : le premier contient un tableau cours, tandis que le
second contient un champ courriel.
Par défaut, MongoDB n’impose pas un schéma strict à tous les documents d’une même collection : deux documents d’une même collection peuvent donc avoir des champs différents.
Cette flexibilité est l’une des grandes différences avec le modèle relationnel. Elle facilite l’évolution des données, mais elle déplace aussi une responsabilité vers l’application, l’API ou le service qui exploite ces données. Il faut alors s’assurer que les documents restent cohérents, que les champs attendus sont bien présents lorsque nécessaire, et que les données sont validées au bon endroit.
Le vocabulaire, en correspondance
Si vous venez du monde relationnel (SQL), la traduction est directe :
| Relationnel (SQL) | MongoDB |
|---|---|
| base de données | base de données |
| table | collection |
| ligne (row) | document |
| colonne (column) | champ (field) |
| clé primaire | _id (un ObjectId par défaut) |
jointure (JOIN) |
imbrication, ou $lookup |
| schéma rigide | schéma souple (défini côté application) |
Les types : la correspondance Mongo
Un champ de document a un type BSON. Voici les types courants et leur équivalent conceptuel :
| Donnée | Type BSON | Exemple |
|---|---|---|
| identifiant | ObjectId |
ObjectId("...") |
| texte | String |
"Dune" |
| nombre | Number (Int32 / Double) |
42, 19.99 |
| vrai / faux | Boolean |
true |
| date | Date |
ISODate("2026-05-12") |
| liste | Array |
["roman", "essai"] |
| objet imbriqué | Object |
{ lat: 45.5, lng: -73.6 } |
| absence de valeur | Null |
null |
Le type Date et le type ObjectId sont ce qui distingue le BSON du JSON pur : du vrai JSON ne connaît ni les dates ni les identifiants natifs.
La décision centrale : imbriquer ou référencer
C’est ici que MongoDB demande une vraie réflexion. En relationnel, une relation entre deux entités se traduit toujours de la même façon : deux tables et une clé étrangère. En MongoDB, vous avez un choix pour chaque relation :
- Imbriquer (embed) — placer les données liées à l’intérieur du document parent.
- Référencer (reference) — stocker un identifiant qui pointe vers un document d’une autre collection.
L’outil ci-dessous rend cette décision manipulable via deux gestes :
- Regrouper (imbriquer) — glissez une entité à l’intérieur d’une autre. L’entité déposée devient un sous-document du conteneur et quitte le niveau racine.
- Lier (référencer) — cliquez une entité, puis une seconde. Un champ de référence (
ObjectId) apparaît dans une section Dépendances, sous les attributs propres de la première.
La petite bascule 1 / N sur chaque relation ajuste la cardinalité (un sous-document seul ou un tableau ; un identifiant ou un tableau d’identifiants).
Interroger la base de données
Lire avec find
La méthode find permet de rechercher et afficher des documents (données) dans une collection MongoDB selon certains critères.
Appelée sans argument, elle les renvoie tous :
db.livres.find() // tous les documents de la collection "livres"
Forme générale
db[collection].find(<query>, <projection>, <options>)
La méthode find() est appelé sur une collection spécifique, accédée via db[collection]. Ici collection est remplacé par le nom de la collection.
La méthode find() utilise les paramètres (optionnels) suivant:
query: Filtre de sélection. Définit les conditions que les documents doivent respecter pour être retournés. Si ce paramètre est omis ou vide ({}), MongoDB renvoie tous les documents de la collectionprojection: Spécifie quels champs des documents doivent être affichés dans le résultat. Si tu ne précises rien, tous les champs seront affichés.
Cas d’utilisation
Supposons une collection employes avec les données suivante
- id : number
- nom : string
- prenom : string
- poste : string
- salaire: double
- ville : string
- competences : Array(string)
- date_embauche: date
- actif: boolean
1. Le paramètre query
Le paramètre query correspond au filtre utilisé pour sélectionner les documents. Il peut être simple ou très complexe grâce aux opérateurs de requête.
Exemple:
// Récupérer tous les documents (employés):
db["employes"].find({})
//Trouver un employé nommé "Bertrand"
db["employes"].find({ nom: "Bertrand" })
// Trouver tous les employés dont le salaire dépasse 50,000
db["employes"].find({ salaire: { $gt: 50000 } })
// Trouver les employés actif habitant à Montréal
db["employes"].find({ actif: true, ville : "Montréal" })
// Trouver ceux ayant une compétences précise
db["employes"].find({ competences: "Python" })
2. Le paramètre projection
Le paramètre projection permet de choisir les champs à afficher. Par défaut, tous les champs sont retournés, mais tu peux limiter les résultats pour plus de clarté ou d’efficacité.
1signifie que le champ est inclus_id: 0supprime l’affichage de l’identifiant automatique.
// Afficher uniquement le nom et le salaire
db["employes"].find({}, { nom: 1, salaire: 1, _id: 0 })
// Afficher uniquement le nom et la ville des employés actifs
db["employes"].find({ actif: true }, { nom: 1, ville: 1, _id: 0 })
// Afficher seulement les compétences
db["employes"].find(
{ nom: "Claudia" },
{ competences: 1, _id: 0 }
)
Égalité et comparaison
Sans opérateur, un champ teste l’égalité stricte :
db.livres.find({ categorie: "roman" }) // categorie === "roman"
Les opérateurs de comparaison élargissent le test :
| Opérateur | Sens | Exemple |
|---|---|---|
$eq |
égal (l’égalité implicite) | { annee: { $eq: 2020 } } |
$ne |
différent | { categorie: { $ne: "essai" } } |
$gt / $gte |
supérieur / ou égal | { annee: { $gte: 2000 } } |
$lt / $lte |
inférieur / ou égal | { annee: { $lt: 1950 } } |
$in |
dans une liste | { quartier: { $in: ["Plateau", "Verdun"] } } |
$nin |
hors d’une liste | { categorie: { $nin: ["essai", "BD"] } } |
// Livres parus depuis 2000, mais pas les essais
db.livres.find({ annee: { $gte: 2000 }, categorie: { $ne: "essai" } })
Plusieurs champs dans le même filtre sont reliés par un ET implicite : tous doivent être satisfaits. Le filtre ci-dessus exige à la fois l’année et la catégorie.
Opérateurs logiques
Pour un OU — ou des combinaisons plus libres — on passe par les opérateurs logiques, qui prennent un tableau de conditions :
| Opérateur | Sens |
|---|---|
$or |
au moins une condition vraie |
$and |
toutes vraies (souvent implicite) |
$nor |
aucune vraie |
$not |
nie une condition sur un champ |
// Les romans, ou n'importe quel livre paru avant 1950
db.livres.find({
$or: [
{ categorie: "roman" },
{ annee: { $lt: 1950 } },
],
})
$and explicite ne sert qu’à imposer deux conditions sur le même champ : { $and: [ { annee: { $gte: 2000 } }, { annee: { $lt: 2010 } } ] } — impossible à écrire comme deux clés annee dans un seul objet.
VérificationÉcrire le filtre : les événements de type emprunt survenus depuis le 1er mai 2026.
db.evenements.find({ type: "emprunt", date: { $gte: ISODate("2026-05-01") } }). Les deux champs forment un ET implicite : on veut les documents dont le type vaut "emprunt" et dont la date est postérieure ou égale au 1er mai. $gte accepte une Date (ISODate) aussi naturellement qu’un nombre.
Opérations CRUD
On peut lire et écrire dans la base de données sans aucune bibliothèque, directement dans mongosh (le pilote Node mongodb expose les mêmes méthodes).
Les quatre opérations CRUD :
Create — insérer
db.boites.insertOne({ nom: "Boîte du parc", quartier: "Rosemont" });
db.boites.insertMany([
{ nom: "Coin lecture", quartier: "Plateau" },
{ nom: "Halte-livres", quartier: "Verdun" },
]);
insertOne renvoie l’_id généré (insertedId) ; la BD le crée pour vous si vous ne le fournissez pas.
Read — lire
db.boites.find({ quartier: "Plateau" }); // un curseur sur les documents correspondants
db.boites.findOne({ _id: ObjectId("…") }); // un seul document, ou null
db.boites.countDocuments({ quartier: "Plateau" });
Le filtre accepte exactement les opérateurs de la section précédente.
Update — modifier
Une écriture combine un filtre (quels documents) et un document de mise à jour bâti avec des opérateurs : $set (affecter un champ), $unset (le retirer), $inc (incrémenter un nombre) :
// Renommer une boîte précise
db.boites.updateOne(
{ _id: ObjectId("…") },
{ $set: { nom: "Boîte rénovée" } }
);
// Reclasser tout un quartier d'un coup (plusieurs documents)
db.boites.updateMany(
{ quartier: "Plateau" },
{ $set: { quartier: "Le Plateau-Mont-Royal" } }
);
Sans $set, MongoDB remplace le document entier par l’objet fourni. updateOne(filtre, { nom: "X" }) effacerait quartier et tous les autres champs. On veut donc presque toujours $set.
Delete — supprimer
db.boites.deleteOne({ _id: ObjectId("…") }); // le premier qui correspond
db.boites.deleteMany({ quartier: "Verdun" }); // tous ceux qui correspondent
Intégration pas à pas avec Express
On va maintenant brancher MongoDB à un serveur Express — en reprenant l’API de boîtes à livres construite précédemment. Jusqu’ici, les données vivaient dans un tableau en mémoire ; on va les persister dans MongoDB.
Mongoose est une bibliothèque ODM (Object Data Modeling) qui ajoute une couche au-dessus de MongoDB : des schémas définis côté application, de la validation, du type casting et une API de requêtes plus confortable.
Étape 1 — Installer Mongoose
npm install mongoose
On suppose un cluster MongoDB Atlas ou une instance locale. La connexion se fait via une URI.
Étape 2 — Se connecter à la base de données
On isole la connexion dans son module, appelé une fois au démarrage :
// Fichier src/db.js
import mongoose from "mongoose";
export async function connecterDB() {
const uri = process.env.MONGO_URI;
await mongoose.connect(uri);
console.log("Connecté à MongoDB");
}
mongoose.connect() retourne une Promise — d’où le await. On l’appelle dans le point d’entrée, avant de démarrer le serveur :
// Fichier index.js
import app from "./src/app.js";
import { connecterDB } from "./src/db.js";
await connecterDB(); // d'abord la BD...
app.listen(3000, () => { // ...puis le serveur
console.log("Serveur sur http://localhost:3000");
});
Connecter la BD avant d’écouter garantit qu’aucune requête n’arrive avant que la base de données soit prête.
Étape 3 — Définir un schéma et un modèle
Le schéma décrit la forme attendue d’un document ; le modèle est l’objet par lequel on interroge la collection. C’est ici qu’on réintroduit la structure que MongoDB ne force pas :
// Fichier src/models/Boite.js
import mongoose from "mongoose";
const boiteSchema = new mongoose.Schema({
nom: { type: String, required: true },
quartier: { type: String, default: "inconnu" },
});
// "Boite" → collection "boites" (Mongoose met au pluriel et en minuscules)
export default mongoose.model("Boite", boiteSchema);
Remarquez la correspondance avec les types vus plus haut : String est le type BSON, required et default sont des règles que Mongoose applique côté application.
Étape 4 — Utiliser le modèle dans les routes
On remplace le tableau en mémoire par des appels au modèle. Toutes les méthodes sont asynchrones, donc on utilise async/await partout :
// Fichier src/routes/boites.js
import { Router } from "express";
import Boite from "../models/Boite.js";
const router = Router();
// Lister (avec filtre optionnel par quartier)
router.get("/", async (req, res) => {
const filtre = req.query.quartier ? { quartier: req.query.quartier } : {};
const boites = await Boite.find(filtre);
res.json(boites);
});
// Détail d'une boîte
router.get("/:id", async (req, res) => {
const boite = await Boite.findById(req.params.id);
if (!boite) {
return res.status(404).json({ error: "Boîte introuvable" });
}
res.json(boite);
});
// Créer
router.post("/", async (req, res) => {
try {
const boite = await Boite.create(req.body); // valide selon le schéma
res.status(201).json(boite);
} catch (err) {
res.status(400).json({ error: err.message }); // échec de validation
}
});
export default router;
Étape 5 — Compléter avec modifier et supprimer
// Modifier
router.put("/:id", async (req, res) => {
const boite = await Boite.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true } // renvoie la version à jour, valide les champs
);
if (!boite) {
return res.status(404).json({ error: "Boîte introuvable" });
}
res.json(boite);
});
// Supprimer
router.delete("/:id", async (req, res) => {
const boite = await Boite.findByIdAndDelete(req.params.id);
if (!boite) {
return res.status(404).json({ error: "Boîte introuvable" });
}
res.status(204).end();
});
Ce qui a changé
Le squelette de l’API n’a pas bougé : mêmes routes, mêmes verbes, mêmes codes de statut. Seul le contenu des gestionnaires a changé — d’un tableau en mémoire à des appels Mongoose. C’est exactement pourquoi on avait isolé les données dans leur module : remplacer la source de données ne touche ni au routeur, ni à l’app, ni au point d’entrée.
VérificationPourquoi les gestionnaires de routes sont-ils tous devenus async après l'ajout de MongoDB ?
Parce que toute opération sur la BD passe par le réseau — c’est de l’I/O, donc asynchrone.
Boite.find(), create(), findById() retournent des Promises ; il faut await leur résultat. Avec le tableau en mémoire, l’accès était synchrone et immédiat ; avec une vraie base de données, on attend une réponse. C’est le même passage au non-bloquant que pour fetch.