🟢 Node.js · Débutant

API REST avec Express.js + MongoDB

⏱ 40 minutes🟢 Node.js 20🍃 MongoDB📦 Express.js 4

Express.js est le framework web Node.js le plus populaire. Ce tutoriel construit une API CRUD complète pour gérer des articles de blog : création, lecture, mise à jour, suppression, avec validation, middlewares et gestion des erreurs propre.

1. Initialiser le projet

Terminal
mkdir api-blog && cd api-blog
npm init -y

npm install express mongoose dotenv cors helmet morgan joi
npm install --save-dev nodemon jest supertest
package.json (scripts)
{
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "test": "jest --runInBand"
  }
}

2. Structure du projet

Arborescence
src/
├── server.js           # Point d'entrée
├── app.js              # Configuration Express
├── models/
│   └── Article.js      # Schéma Mongoose
├── routes/
│   └── articles.js     # Routes CRUD
├── middlewares/
│   ├── validate.js     # Validation Joi
│   └── errorHandler.js # Gestion des erreurs
└── __tests__/
    └── articles.test.js

3. Modèle Mongoose

📖 Terme : Schema (Schéma Mongoose)

Définition : Blueprint qui définit la structure d'un document MongoDB : types de champs, validations, valeurs par défaut et méthodes.

But : Imposer une structure et des règles aux données flexibles de MongoDB.

Pourquoi ici : Bien que MongoDB soit sans schéma, Mongoose impose un schéma pour éviter l'chaos et garder l'intégrité des données côté application.

📖 Terme : ODM (Object Document Mapper)

Définition : Couche d'abstraction qui mappe les documents MongoDB à des objets JavaScript (comme un ORM pour SQL).

But : Simplifier l'interaction avec MongoDB en utilisant des méthodes plutôt que du code de requête direct.

Pourquoi ici : Mongoose est l'ODM le plus populaire pour Node.js + MongoDB, offrant validation, middlewares et requêtes type.

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

const articleSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, 'Le titre est obligatoire'],
    trim: true,
    maxlength: [200, 'Le titre ne doit pas dépasser 200 caractères']
  },
  content: {
    type: String,
    required: [true, 'Le contenu est obligatoire'],
    minlength: [10, 'Le contenu doit faire au moins 10 caractères']
  },
  author: {
    type: String,
    required: true
  },
  tags: [{
    type: String,
    lowercase: true,
    trim: true
  }],
  published: {
    type: Boolean,
    default: false
  },
  views: {
    type: Number,
    default: 0
  }
}, {
  timestamps: true,  // Ajoute createdAt et updatedAt automatiquement
  versionKey: false
});

// Index pour la recherche full-text
articleSchema.index({ title: 'text', content: 'text' });

// Méthode d'instance
articleSchema.methods.getSummary = function() {
  return `${this.title} — ${this.content.substring(0, 100)}...`;
};

module.exports = mongoose.model('Article', articleSchema);
Le schéma définit la structure : chaque document aura title, content, author, etc. Les validateurs (required, maxlength, minlength) s'exécutent avant de sauvegarder en base. Les timestamps ajoutent automatiquement createdAt et updatedAt.

4. Configuration Express et middlewares

📖 Terme : Middleware

Définition : Fonction qui intercept la requête HTTP avant qu'elle n'atteigne le gestionnaire de route, effectue une action (validation, logs, sécurité), puis passe la requête au middleware suivant avec next().

But : Découpler les préoccupations transversales (sécurité, logs, authentification) de la logique métier.

Pourquoi ici : Express fonctionne comme une chaîne de middlewares : chaque app.use() ajoute un maillon à cette chaîne. La requête traverse tous les middlewares dans l'ordre jusqu'à atteindre la route.

Chaîne de middlewares : requête → helmet() (sécurité) → cors() (CORS) → morgan() (logs) → express.json() (parsing) → vos routes → gestionnaire d'erreurs
src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const articleRoutes = require('./routes/articles');
const errorHandler = require('./middlewares/errorHandler');

const app = express();

// ── Middlewares de sécurité ──
app.use(helmet());          // Headers de sécurité HTTP
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
}));
app.use(morgan('dev'));      // Logs des requêtes
app.use(express.json());     // Parser JSON

// ── Routes ──
app.use('/api/articles', articleRoutes);
app.get('/health', (req, res) => res.json({ status: 'ok' }));

// ── 404 ──
app.use((req, res) => {
  res.status(404).json({ error: `Route ${req.method} ${req.path} introuvable` });
});

// ── Gestionnaire d'erreurs global ──
app.use(errorHandler);

module.exports = app;

5. Routes CRUD

📖 Terme : Validation

Définition : Vérification que les données envoyées par le client respectent les règles métier (types, formats, longueurs) avant traitement.

But : Rejeter les données malformées tôt, avant qu'elles ne polluent la base de données.

Pourquoi ici : Joi offre une validation déclarative côté serveur, indépendante du schéma Mongoose, pour avoir deux couches de protection.

📖 Terme : Pagination

Définition : Technique de division des résultats en pages : on retourne un sous-ensemble (ex: 10 résultats) au lieu de tous les résultats.

But : Réduire la charge mémoire et réseau, améliorer la performance des API.

Pourquoi ici : skip((page - 1) * limit) saute les premiers N documents, limit() en retourne au maximum M. Compromis : pagination complète mais plus lente que keyset pagination.

src/routes/articles.js
const express = require('express');
const router = express.Router();
const Article = require('../models/Article');
const Joi = require('joi');

// ── Validation Joi ──
const articleSchema = Joi.object({
  title: Joi.string().max(200).required(),
  content: Joi.string().min(10).required(),
  author: Joi.string().required(),
  tags: Joi.array().items(Joi.string()),
  published: Joi.boolean()
});

const validate = (schema) => (req, res, next) => {
  const { error } = schema.validate(req.body, { abortEarly: false });
  if (error) {
    return res.status(422).json({
      error: 'Données invalides',
      details: error.details.map(d => d.message)
    });
  }
  next();
};

// ── GET /api/articles — avec pagination et filtres ──
router.get('/', async (req, res, next) => {
  try {
    const { page = 1, limit = 10, tag, published, search } = req.query;
    const filter = {};

    if (tag) filter.tags = tag;
    if (published !== undefined) filter.published = published === 'true';
    if (search) filter.$text = { $search: search };

    const [articles, total] = await Promise.all([
      Article.find(filter)
        .sort({ createdAt: -1 })
        .skip((Number(page) - 1) * Number(limit))
        .limit(Number(limit))
        .select('-content'),  // Exclure le contenu pour la liste
      Article.countDocuments(filter)
    ]);

    res.json({
      data: articles,
      pagination: {
        page: Number(page),
        limit: Number(limit),
        total,
        pages: Math.ceil(total / Number(limit))
      }
    });
  } catch (err) { next(err); }
});

// ── GET /api/articles/:id ──
router.get('/:id', async (req, res, next) => {
  try {
    const article = await Article.findByIdAndUpdate(
      req.params.id,
      { $inc: { views: 1 } },  // Incrémenter les vues
      { new: true }
    );
    if (!article) return res.status(404).json({ error: 'Article non trouvé' });
    res.json(article);
  } catch (err) { next(err); }
});

// ── POST /api/articles ──
router.post('/', validate(articleSchema), async (req, res, next) => {
  try {
    const article = await Article.create(req.body);
    res.status(201).json(article);
  } catch (err) { next(err); }
});

// ── PATCH /api/articles/:id ──
router.patch('/:id', async (req, res, next) => {
  try {
    const article = await Article.findByIdAndUpdate(
      req.params.id, req.body, { new: true, runValidators: true }
    );
    if (!article) return res.status(404).json({ error: 'Article non trouvé' });
    res.json(article);
  } catch (err) { next(err); }
});

// ── DELETE /api/articles/:id ──
router.delete('/:id', async (req, res, next) => {
  try {
    const article = await Article.findByIdAndDelete(req.params.id);
    if (!article) return res.status(404).json({ error: 'Article non trouvé' });
    res.json({ message: 'Article supprimé', id: req.params.id });
  } catch (err) { next(err); }
});

module.exports = router;

6. Gestionnaire d'erreurs global

📖 Terme : Gestionnaire d'erreurs global

Définition : Middleware Express spécial (4 paramètres : err, req, res, next) qui capture toutes les erreurs jetées ou passées via next(err) dans l'application.

But : Centraliser la gestion des erreurs, en lieu de dupliquer try-catch partout, pour offrir des réponses cohérentes.

Pourquoi ici : Sans gestionnaire global, les erreurs non attrapées causent des crashes. Avec celui-ci, elles retournent des réponses JSON propres au client et des logs au serveur.

Les erreurs Mongoose (ValidationError, CastError, code 11000) ont des signatures spécifiques. Le gestionnaire global les reconnaît et retourne les codes HTTP appropriés (422 pour validation, 400 pour format invalide, 409 pour doublon).
src/middlewares/errorHandler.js
module.exports = (err, req, res, next) => {
  console.error(err.stack);

  // Erreur de validation Mongoose
  if (err.name === 'ValidationError') {
    return res.status(422).json({
      error: 'Données invalides',
      details: Object.values(err.errors).map(e => e.message)
    });
  }

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

  // Doublon (index unique)
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return res.status(409).json({ error: `${field} déjà utilisé` });
  }

  res.status(err.status || 500).json({
    error: err.message || 'Erreur interne du serveur'
  });
};
L'API est disponible sur http://localhost:3000. Testez avec curl ou Postman :