Back-end

Node.js + MongoDB : créer une API scalable

Node-Js Mongodb Api Database Mongoose Rest-Api Javascript Backend Scalabilite Nosql Agregations Express Validation Best-Practices
Node.js + MongoDB : créer une API scalable

Construisez une API production-ready avec Node.js et MongoDB : connexion Mongoose, modèles, validation, agrégations et bonnes pratiques de scalabilité.

Configuration Mongoose

Mongoose est un ODM (Object Document Mapper) pour MongoDB qui facilite la gestion des données. Commencez par installer les dépendances :

npm install mongoose dotenv
npm install -D nodemon

Créez un fichier de configuration pour gérer la connexion à MongoDB :

// config/database.js
const mongoose = require('mongoose');
require('dotenv').config();

const connectDB = async () => {
    try {
        await mongoose.connect(process.env.MONGODB_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true
        });
        console.log('✓ MongoDB connecté');
    } catch (error) {
        console.error('✗ Erreur connexion:', error.message);
        process.exit(1);
    }
};

module.exports = connectDB;
À retenir : Utilisez toujours des variables d'environnement pour les connexions (fichier .env non commité).

Schémas et validation

Les schémas Mongoose définissent la structure des documents et incluent la validation native :

// models/User.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
    name: {
        type: String,
        required: [true, 'Le nom est obligatoire'],
        trim: true,
        minlength: [3, 'Minimum 3 caractères']
    },
    email: {
        type: String,
        required: true,
        unique: true,
        lowercase: true,
        match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Email invalide']
    },
    age: {
        type: Number,
        min: 18,
        max: 120
    },
    createdAt: {
        type: Date,
        default: Date.now
    }
});

module.exports = mongoose.model('User', userSchema);
Note : Les validateurs intégrés (required, min, max, match) s'exécutent avant la sauvegarde. Utilisez la méthode validate() pour valider manuellement.

Modèles et relations

Mongoose supporte les relations One-to-Many et Many-to-Many via les références :

// models/Post.js
const postSchema = new mongoose.Schema({
    title: String,
    content: String,
    author: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
    },
    tags: [{
        type: String,
        enum: ['javascript', 'nodejs', 'mongodb']
    }],
    likes: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User'
    }]
});

module.exports = mongoose.model('Post', postSchema);

Pour récupérer les données liées, utilisez populate() :

// routes/posts.js
const posts = await Post.find()
    .populate('author', 'name email')
    .populate('likes', 'name')
    .lean();

// .lean() retourne un objet JavaScript simple (plus rapide)

Optimisation des requêtes

Les requêtes non optimisées causent des ralentissements. Voici les meilleures pratiques :

ProblèmeSolution
N+1 queriesUtiliser populate() ou aggregation()
Champs inutilesSélectionner uniquement les champs nécessaires
Pas d'indexCréer des index sur les champs fréquemment interrogés
Pas de paginationUtiliser limit() et skip()
// ❌ MAUVAIS - Charge TOUS les utilisateurs
const posts = await Post.find();
for (let post of posts) {
    const user = await User.findById(post.author); // N+1 queries!
}

// ✅ BON - Une seule requête
const posts = await Post.find()
    .populate('author')
    .select('title author');  // Sélectionner uniquement les champs nécessaires

// ✅ EXCELLENT - Avec pagination et indexation
const page = req.query.page || 1;
const limit = 20;
const posts = await Post.find()
    .populate('author')
    .skip((page - 1) * limit)
    .limit(limit)
    .lean();

Créez des index sur les champs interrogés fréquemment :

// models/Post.js
postSchema.index({ createdAt: -1 });
postSchema.index({ author: 1 });
postSchema.index({ title: 'text', content: 'text' }); // Full-text search

Gestion d'erreurs

Gérez les erreurs Mongoose de manière appropriée :

// middleware/errorHandler.js
const handleMongooseError = (error, res) => {
    // Erreur de validation
    if (error.name === 'ValidationError') {
        const messages = Object.values(error.errors)
            .map(err => err.message);
        return res.status(400).json({ errors: messages });
    }

    // Erreur de duplication (unique)
    if (error.code === 11000) {
        const field = Object.keys(error.keyPattern)[0];
        return res.status(400).json({
            error: `${field} doit être unique`
        });
    }

    // Cast error (ID invalide)
    if (error.name === 'CastError') {
        return res.status(400).json({ error: 'ID invalide' });
    }

    // Erreur serveur
    return res.status(500).json({ error: error.message });
};

// routes/users.js
router.post('/users', async (req, res) => {
    try {
        const user = new User(req.body);
        await user.save();
        res.status(201).json(user);
    } catch (error) {
        handleMongooseError(error, res);
    }
});
À retenir : Gérez toujours les erreurs de validation et de duplication pour une API robuste.

Questions fréquentes

Une API scalable repose sur une architecture en couches : routes, contrôleurs, services et modèles Mongoose séparés. Il est recommandé d'utiliser des index MongoDB sur les champs fréquemment filtrés (ex. `email`, `createdAt`), d'activer le pooling de connexions avec `mongoose.connect()` et l'option `maxPoolSize`, et d'implémenter la pagination via `skip()` + `limit()` ou curseurs. Pour 10 000+ requêtes/minute, combinez replica sets MongoDB et clustering Node.js avec PM2.

Mongoose apporte la validation déclarative des schémas, les hooks pre/post (ex. hachage de mot de passe avant save), les virtual fields, les méthodes personnalisées sur les documents, et un système de plugins extensible. Le driver natif est plus rapide (~15 % en benchmarks) car il supprime la couche d'abstraction, mais Mongoose réduit les erreurs de type et accélère le développement. Pour les agrégations complexes, les deux permettent d'utiliser la pipeline MongoDB.

Utilisez le middleware Express d'erreurs centralisé (`app.use((err, req, res, next) => {...})`) combiné aux erreurs Mongoose (`ValidationError`, `CastError`, code 11000 pour les duplicates). Définissez des schémas stricts avec `required`, `min`, `max`, `enum` et des messages d'erreur personnalisés. Ajoutez `express-validator` pour valider les entrées HTTP avant d'atteindre Mongoose. Loggez avec Winston ou Pino en JSON pour faciliter l'analyse en production.

Analysez les requêtes lentes avec `db.setProfilingLevel(1, { slowms: 100 })` et `explain("executionStats")`. Créez des index composés pour les requêtes multi-champs (ex. `{ category: 1, createdAt: -1 }`), utilisez la projection pour ne sélectionner que les champs utiles (`find({}, { name: 1, email: 1 }`), et activez le cache de requêtes. Pour les lectures intensives, déployez des read replicas et orientez les requêtes avec `readPreference: "secondaryPreferred"`.

Partager