Connaître MongoDB côté mongosh n’est que la première moitié du travail : en production, c’est un driver applicatif qui ouvre les connexions, gère le pool, retente les écritures, et expose la session de transaction. Côté Node.js, deux options dominantes — le driver natif mongodb et l’ODM mongoose. Côté Python, le driver synchrone pymongo et son équivalent asynchrone motor. Ce tutoriel détaille comment configurer chaque combinaison sur MongoDB 8.0 LTS pour qu’elle tienne la charge, encaisse un failover de replica set, et respecte les bonnes pratiques de pool, timeout et idempotence.
Prérequis
- MongoDB 8.0 LTS — replica set, ou Atlas M0 minimum.
- Node.js 22 LTS ou supérieur pour la partie Node — Node 20 atteint EOL en avril 2026.
- Python 3.12 ou supérieur pour la partie Python.
- Niveau intermédiaire : vous écrivez déjà du code asynchrone (Promise/async/await en Node, asyncio en Python).
- Temps estimé : 110 minutes pour traverser les 9 étapes.
Étape 1 — Choisir entre driver natif et ODM
Un driver est la couche bas-niveau qui parle le protocole binaire MongoDB : il ouvre les connexions TCP, sérialise les documents en BSON, et expose des méthodes CRUD sans rien imposer côté application. Un ODM (Object Document Mapper) ajoute par-dessus un schéma typé, des hooks de cycle de vie, de la validation, et des relations à la mode ORM.
Côté Node, le driver natif est mongodb — c’est lui qu’utilise mongoose en interne. Vous avez donc le choix entre travailler directement avec lui (verbose mais souple) ou via mongoose (concis mais opinionné). En 2026, mongodb driver est en version 7.x — mongoose est en version 9.x et reste compatible avec le driver 6.x ou 7.x. Côté Python, pymongo est le driver synchrone officiel maintenu par MongoDB Inc. ; motor en est la version asyncio. Pour FastAPI et tout backend asynchrone moderne, c’est motor qui s’impose.
Étape 2 — Installer et brancher en Node.js
L’installation est triviale en npm. La connexion ouvre un connection pool partagé — vous instanciez MongoClient une seule fois au démarrage de l’application, jamais à chaque requête HTTP. Re-créer un client par requête est l’erreur N°1 chez les débutants : le pool ouvre des dizaines de sockets TCP avec handshake TLS, ce qui détruit la latence et déclenche des fuites de descripteurs de fichiers.
// package.json
// npm install mongodb@7
import { MongoClient } from "mongodb";
const uri = process.env.MONGO_URI;
// Format URI replica set :
// mongodb://user:pass@node1,node2,node3/dbname?replicaSet=rs0&authSource=admin
const client = new MongoClient(uri, {
maxPoolSize: 50,
minPoolSize: 5,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 10000,
socketTimeoutMS: 30000,
retryWrites: true,
retryReads: true,
w: "majority"
});
await client.connect();
const db = client.db("ecom");
const produits = db.collection("produits");
// Au shutdown propre du process
process.on("SIGTERM", async () => { await client.close(); process.exit(0); });
Cinq options à connaître par cœur. maxPoolSize: 50 est un plafond conseillé pour la plupart des applis web — au-delà, le serveur consomme inutilement des threads. minPoolSize: 5 garde 5 connexions chaudes pour absorber les pics. retryWrites: true retente une fois automatiquement les écritures idempotentes en cas de panne réseau ou de bascule de primary. w: "majority" attend l’acknowledgement d’une majorité de nœuds avant de considérer l’écriture validée — c’est la durabilité réelle, pas l’acceptation côté primary uniquement.
Étape 3 — Mongoose : schéma, modèle, validation
Mongoose ajoute un schéma typé sur Node. L’avantage : validation à l’écriture, IntelliSense côté TypeScript, hooks pre/post sur les opérations. L’inconvénient : une couche d’abstraction qui peut masquer le comportement réel du driver — utile à connaître pour debugger un cas limite.
// npm install mongoose@9
import mongoose, { Schema, model } from "mongoose";
await mongoose.connect(process.env.MONGO_URI, {
maxPoolSize: 50,
serverSelectionTimeoutMS: 5000,
retryWrites: true
});
const produitSchema = new Schema({
nom: { type: String, required: true, minlength: 2, maxlength: 200 },
prix_xof: { type: Number, required: true, min: 0 },
categorie: { type: String, enum: ["smartphone", "tissu", "alimentaire"] },
stock: { type: Number, default: 0, min: 0 },
attributs: { type: Schema.Types.Mixed },
cree_le: { type: Date, default: Date.now }
}, { timestamps: true });
produitSchema.index({ categorie: 1, prix_xof: -1 });
const Produit = model("Produit", produitSchema);
// CRUD
const nouveau = await Produit.create({ nom: "Test", prix_xof: 15000, categorie: "tissu" });
const liste = await Produit.find({ categorie: "smartphone" }).limit(10).lean();
await Produit.updateOne({ _id: nouveau._id }, { $inc: { stock: 1 } });
L’option .lean() retourne des objets JavaScript bruts au lieu de documents Mongoose hydratés — gain de performance et de mémoire significatif quand vous n’avez pas besoin des hooks ou de l’API document. La règle empirique : .lean() sur toute lecture où vous n’avez pas besoin d’appeler .save() ensuite. Référence : Mongoose Lean Queries.
Étape 4 — Transactions avec Mongoose
Une transaction Mongoose passe par une session obtenue de la connexion. Le pattern est identique à celui du driver natif : démarrer, exécuter les opérations avec la session, commiter ou annuler.
const session = await mongoose.startSession();
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
try {
await Compte.updateOne(
{ _id: idDebit, solde: { $gte: 10000 } },
{ $inc: { solde: -10000 } },
{ session }
);
await Compte.updateOne(
{ _id: idCredit },
{ $inc: { solde: +10000 } },
{ session }
);
await Operation.create([{
type: "virement",
debit: idDebit,
credit: idCredit,
montant: 10000
}], { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
await session.endSession();
}
La clause solde: { $gte: 10000 } dans le filtre du premier updateOne est essentielle : elle garantit atomiquement que le compte avait bien de quoi débiter. Si le solde a déjà été ponctionné par une autre opération concurrente, l’updateOne renvoie matchedCount: 0 — c’est à votre code de détecter ce cas et d’aborter la transaction. Sans ce filtre, vous risquez un solde négatif : la transaction réussit mais le résultat métier est faux.
Étape 5 — PyMongo : driver synchrone Python
Côté Python, pymongo est le driver de référence pour les scripts batch, les notebooks Jupyter, les jobs cron. Il est synchrone — chaque opération bloque jusqu’à la réponse. Parfait pour l’ETL et l’analyse, moins adapté aux backends web concurrents.
# pip install pymongo[srv]
from pymongo import MongoClient, WriteConcern, ReadPreference
from pymongo.errors import DuplicateKeyError, AutoReconnect
import os
client = MongoClient(
os.environ["MONGO_URI"],
maxPoolSize=50,
minPoolSize=5,
serverSelectionTimeoutMS=5000,
connectTimeoutMS=10000,
socketTimeoutMS=30000,
retryWrites=True,
retryReads=True,
w="majority"
)
db = client["ecom"]
produits = db["produits"]
# CRUD basique
produits.insert_one({"nom": "Test", "prix_xof": 15000, "categorie": "tissu"})
# Lecture avec readPreference dirigée vers les secondaries
produits_lus = list(
db.get_collection("produits", read_preference=ReadPreference.SECONDARY_PREFERRED)
.find({"actif": True})
.limit(10)
)
Le suffixe pymongo[srv] à l’installation ajoute la dépendance dnspython requise pour les URIs mongodb+srv:// typiques d’Atlas — sans elle, la connexion échoue avec un message peu clair. La règle pratique : toujours installer pymongo[srv] sauf si vous certifiez ne jamais utiliser SRV. Référence : PyMongo Installation.
Étape 6 — Motor : driver asyncio
Pour FastAPI, Starlette, ou tout backend Python asynchrone, motor remplace pymongo. L’API est identique à 95 %, à condition d’ajouter await devant chaque opération qui touche le réseau. motor est maintenu par MongoDB Inc. et reste la voie officielle pour Python async.
# pip install motor
from motor.motor_asyncio import AsyncIOMotorClient
import os, asyncio
client = AsyncIOMotorClient(
os.environ["MONGO_URI"],
maxPoolSize=50,
serverSelectionTimeoutMS=5000,
retryWrites=True
)
db = client["ecom"]
produits = db["produits"]
async def lister_smartphones():
cursor = produits.find({"categorie": "smartphone", "actif": True}).limit(20)
return await cursor.to_list(length=20)
# Intégration FastAPI (lifespan recommandé depuis FastAPI 0.93)
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# Forcer l'ouverture initiale du pool au démarrage
await client.admin.command("ping")
yield
# Fermeture propre au shutdown
client.close()
app = FastAPI(lifespan=lifespan)
@app.get("/produits")
async def get_produits():
return await lister_smartphones()
La fonction lifespan au démarrage est un détail souvent oublié (depuis FastAPI 0.93, @app.on_event("startup") est déprécié au profit du lifespan context manager) : sans elle, le premier client à frapper l’API attend l’établissement du pool et perçoit une latence anormalement élevée. Un ping initial déclenche l’ouverture des connexions et l’identification du primary ; les requêtes suivantes profitent d’un pool déjà chaud.
Étape 7 — Pool de connexions : dimensionner correctement
Le pool est le réservoir de sockets TCP réutilisables. Trop petit, il devient un goulot d’étranglement quand le trafic monte. Trop grand, il sature le serveur en sockets ouvertes et exclut d’autres applications. La taille optimale dépend du trafic et de la concurrence applicative.
| Trafic typique | maxPoolSize |
minPoolSize |
Notes |
|---|---|---|---|
| API CRUD simple, < 100 req/s | 20 | 2 | Défaut conservateur, suffisant pour MVP |
| API e-commerce, 100-1000 req/s | 50 | 5 | Profile recommandé pour la majorité des cas |
| API analytics, agrégations longues | 100 | 10 | Compenser le temps de réponse plus long |
| Workers de traitement batch | 200 | 20 | Si les workers parallélisent agressivement |
Pour le calcul précis : poolSize ≈ requêtes_par_seconde × latence_moyenne_en_secondes. À 500 req/s avec 80 ms de latence moyenne, on a besoin de 40 connexions actives au pic — maxPoolSize: 50 donne une marge raisonnable. Augmenter au-delà sans mesure est contre-productif.
Étape 8 — Gérer les erreurs réseau et le failover
En production sur replica set, le primary peut basculer : arrêt maintenance, panne hardware, partition réseau. Le driver doit pouvoir détecter, retenter, et reconnecter sans intervention. Trois mécanismes coopèrent.
Un, le server discovery : le driver maintient une vue à jour de la topologie du replica set via des heartbeats. Quand le primary disparaît, il détecte l’élection en cours et met les écritures en attente. Deux, les retryable writes (retryWrites: true) : les opérations idempotentes (insertOne, updateOne avec $set, deleteOne) sont retentées automatiquement une fois. Trois, les retryable reads (retryReads: true) : même mécanisme côté lectures.
// Node.js — gestion explicite de l'erreur de réseau
import { MongoNetworkError, MongoServerSelectionError } from "mongodb";
async function inserer(doc, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await produits.insertOne(doc);
} catch (err) {
if (err instanceof MongoNetworkError || err instanceof MongoServerSelectionError) {
await new Promise(r => setTimeout(r, 200 * (i + 1)));
continue;
}
throw err;
}
}
throw new Error("MongoDB indisponible après 3 retries");
}
Notez le backoff linéaire (200 * (i + 1) ms) entre chaque retry. Sans backoff, vous arrosez le serveur de tentatives pendant qu’il essaye d’élire un nouveau primary — vous prolongez l’incident. Un backoff exponentiel (200 * 2^i) est encore plus prudent sur des incidents longs.
Étape 9 — Logs, monitoring, sondes
Un driver bien configuré sans monitoring est aveugle aux incidents réels. Trois sources de signal : les logs structurés du driver, les commandes serverStatus et currentOp côté serveur, et l’intégration avec Atlas ou un agent Prometheus pour les déploiements self-hosted.
// Node.js — instrumentation OpenTelemetry pour les opérations Mongo
import { MongoClient } from "mongodb";
import { MongoDBInstrumentation } from "@opentelemetry/instrumentation-mongodb";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
const provider = new NodeTracerProvider();
provider.register();
registerInstrumentations({
instrumentations: [
new MongoDBInstrumentation({ enhancedDatabaseReporting: true })
]
});
const client = new MongoClient(uri);
await client.connect();
// Chaque opération produit maintenant un span avec:
// - db.statement, db.collection, db.operation
// - durée, statut succès/échec
# Python — exporter Prometheus du pool MongoDB
from prometheus_client import Gauge, start_http_server
pool_size = Gauge("mongodb_pool_size", "Connexions actives dans le pool")
start_http_server(9090)
# Mise à jour périodique
import asyncio
async def refresh_metrics():
while True:
status = await client.admin.command("serverStatus")
pool_size.set(status["connections"]["current"])
await asyncio.sleep(15)
asyncio.create_task(refresh_metrics())
Trois métriques à scruter en production. connections.current : si elle frôle maxPoolSize, le pool sature et il faut l’élargir ou réduire la charge. opLatencies.reads.latency : si elle dépasse 50 ms en moyenne, soit les requêtes ne sont pas indexées, soit le serveur est sous-dimensionné. repl.lag : si un secondary a plus de quelques secondes de retard sur le primary, il sort du quorum pour les écritures w: majority et la latence des écritures critique grimpe.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Pool saturé sous trafic normal | maxPoolSize trop bas ou clients re-créés à chaque requête |
Vérifier que MongoClient est singleton, augmenter à 50 |
ServerSelectionTimeoutError au démarrage |
URI mal formé ou DNS SRV non résolu | Installer pymongo[srv] ou dnspython, vérifier la résolution DNS |
Transaction WriteConflict répété |
Concurrence sur les mêmes documents | Retry boucle avec backoff, ou pattern optimistic locking |
| Lectures avec données obsolètes | readPreference: secondary sans maxStalenessSeconds |
Forcer maxStalenessSeconds: 90 ou repasser sur le primary |
BulkWriteError sur insertMany |
Doublon sur index unique | Utiliser ordered: false pour ne pas tout abandonner au premier échec |
| Mongoose hydrate des millions de docs | Lecture sans .lean() |
Ajouter .lean() partout où on ne fait que de la projection |
FAQ
Q : Driver natif mongodb ou ODM mongoose ?
R : Mongoose pour les apps standard avec schéma stable et validation forte (idéal pour les MVP et SaaS). Driver natif pour les workloads avancés (pipelines complexes, performance critique, contrôle fin du BSON).
Q : PyMongo synchrone ou Motor async ?
R : PyMongo pour les scripts batch, ETL, jobs cron. Motor pour FastAPI et tout serveur asyncio. La même base de code expose les deux — pas de réécriture majeure pour migrer.
Q : Faut-il fermer le client à chaque requête ?
R : Non, jamais. Le client est un singleton ouvert au démarrage du process et fermé proprement au SIGTERM. Fermer puis rouvrir entre les requêtes annule tout le bénéfice du pool.
Q : Comment passer un ObjectId dans une URL ?
R : Le format hexadécimal de 24 caractères se passe tel quel dans une URL. Côté serveur, le convertir explicitement : new ObjectId(req.params.id) en Node, ObjectId(id_str) en Python. Sans cette conversion, MongoDB cherche une chaîne là où il attend un ObjectId et ne trouve rien.
Q : Transactions ou opérations atomiques ?
R : Préférer atomique tant que possible. Une seule update avec opérateurs ($inc, $push, $addToSet) sur un seul document est atomique nativement et plus performante qu’une transaction. La transaction reste indispensable dès que deux documents distincts doivent évoluer ensemble.
Pour aller plus loin
- MongoDB : NoSQL en pratique — vue d’ensemble du parcours.
- Replica sets MongoDB et élections — comprendre le contexte que le driver doit gérer.
- Indexes MongoDB et performance — un driver bien configuré tape sur des indexes bien posés.
- Node.js Driver — Manual — documentation officielle.
- PyMongo Documentation — référence officielle Python.
- Mongoose Documentation — guide officiel de l’ODM Node.