1. Principes REST

REST (Representational State Transfer) est un style d'architecture pour les APIs web, défini par Roy Fielding en 2000. Une API qui respecte les contraintes REST est dite RESTful.

Les 5 contraintes clés de REST

  • Sans état (Stateless) — chaque requête contient toutes les infos nécessaires
  • Interface uniforme — ressources identifiées par des URLs, méthodes HTTP sémantiques
  • Ressources nommées — noms (pluriels), pas de verbes dans les URLs
  • Représentation — les ressources sont représentées en JSON (ou XML)
  • Cacheable — les réponses indiquent si elles peuvent être mises en cache

Méthodes HTTP sémantiques

MéthodeActionURL exempleStatus réussiteIdempotent ?
GET Lire GET /books / GET /books/1 200 OK Oui
POST Créer POST /books 201 Created Non
PUT Remplacer (complet) PUT /books/1 200 OK Oui
PATCH Modifier (partiel) PATCH /books/1 200 OK Non nécessairement
DELETE Supprimer DELETE /books/1 204 No Content Oui

URLs REST : bonnes pratiques

// ✅ Bonnes URLs REST — noms pluriels, pas de verbes
GET    /api/books           // liste
GET    /api/books/42        // un livre
POST   /api/books           // créer
PUT    /api/books/42        // modifier complet
PATCH  /api/books/42        // modifier partiel
DELETE /api/books/42        // supprimer

// Ressources imbriquées (relations)
GET    /api/users/5/books   // livres de l'utilisateur 5
POST   /api/users/5/books   // créer un livre pour user 5

// ❌ Mauvaises URLs — verbes dans l'URL
GET    /api/getBooks        // ❌ verbe
POST   /api/createBook      // ❌ verbe
DELETE /api/deleteBook/42   // ❌ verbe
Une API RESTful bien conçue est intuitive : un développeur qui la découvre peut deviner les endpoints. La cohérence dans le nommage et les status codes est la clé.

2. Status Codes HTTP corrects

Les status codes HTTP communiquent le résultat d'une requête de manière standardisée. Utiliser le bon code permet aux clients (frontend, outils, autres APIs) de comprendre ce qui s'est passé sans lire le corps de la réponse.

Status codes par catégorie

CodeNomQuand l'utiliser
200OKGET / PUT / PATCH réussi
201CreatedPOST réussi — ressource créée
204No ContentDELETE réussi — pas de corps de réponse
400Bad RequestDonnées invalides, champs manquants
401UnauthorizedNon authentifié — token manquant/expiré
403ForbiddenAuthentifié mais non autorisé
404Not FoundRessource introuvable
409ConflictDoublon — email déjà utilisé
422Unprocessable EntitySyntaxe OK mais données sémantiquement invalides
500Internal Server ErrorErreur serveur inattendue

Utiliser le bon status code à chaque cas

// Exemple complet : router POST /books avec tous les status codes
router.post('/', (req, res) => {
  const { title, author, price, isbn } = req.body;

  // 400 — Champs requis manquants
  if (!title || !author)
    return res.status(400).json({
      success: false,
      error: 'Les champs title et author sont requis'
    });

  // 422 — Données invalides sémantiquement
  if (price !== undefined && (isNaN(price) || price < 0))
    return res.status(422).json({
      success: false,
      error: 'Le prix doit être un nombre positif'
    });

  // 409 — Conflit / doublon
  if (isbn && books.find(b => b.isbn === isbn))
    return res.status(409).json({
      success: false,
      error: 'Un livre avec cet ISBN existe déjà'
    });

  // 201 — Création réussie
  const book = { id: nextId++, title, author, price: price ?? 0, isbn };
  books.push(book);
  return res.status(201).json({ success: true, data: book });
});

// 204 — Suppression (pas de body)
router.delete('/:id', (req, res) => {
  const idx = books.findIndex(b => b.id === +req.params.id);
  if (idx === -1)
    return res.status(404).json({ success: false, error: 'Livre non trouvé' });
  books.splice(idx, 1);
  res.status(204).send(); // Pas de .json() ici !
});
Erreur fréquente : utiliser 200 pour tous les succès. Les clients qui lisent les headers HTTP (Axios, fetch avec vérification) traiteront incorrectement une création si vous retournez 200 au lieu de 201.

3. Réponses JSON standardisées

Une API professionnelle retourne un format de réponse cohérent et prévisible. Cela simplifie le code client et le débogage. Adoptez un format dès le début et tenez-vous y.

Format recommandé

// Succès : liste avec pagination
{
  "success": true,
  "data": [ ... ],
  "meta": {
    "total": 47,
    "page": 2,
    "limit": 10,
    "totalPages": 5
  }
}

// Succès : ressource unique
{
  "success": true,
  "data": { "id": 1, "title": "Node.js", ... }
}

// Erreur
{
  "success": false,
  "error": "Message lisible par un humain",
  "details": [                // optionnel — erreurs par champ
    { "field": "email", "message": "Format email invalide" },
    { "field": "price", "message": "Doit être positif" }
  ]
}

Helper de réponse réutilisable

// helpers/respond.js — Centralise le format de réponse

function respond(res, status, data, meta = null) {
  const body = { success: status < 400 };
  if (status < 400) {
    body.data = data;
    if (meta) body.meta = meta;
  } else {
    body.error   = data;          // data = message d'erreur
    if (meta) body.details = meta; // meta = tableau des erreurs par champ
  }
  return res.status(status).json(body);
}

module.exports = respond;

// ─────────────────────────────────────────────
// Utilisation dans les routes :
const respond = require('./helpers/respond');

// Succès
respond(res, 200, books);                    // liste
respond(res, 200, book);                     // ressource
respond(res, 201, newBook);                  // création
// Avec meta pagination
respond(res, 200, page, { total, page, limit, totalPages });

// Erreurs
respond(res, 404, 'Livre non trouvé');
respond(res, 400, 'Données invalides', [
  { field: 'title',  message: 'Champ requis' },
  { field: 'price',  message: 'Doit être un nombre positif' }
]);
respond(res, 500, 'Erreur interne du serveur');
En internalisant le format dans un helper, un seul endroit à modifier si vous voulez changer la structure. Ex: ajouter un champ timestamp ou requestId à toutes les réponses.

4. Validation des entrées

Ne faites jamais confiance aux données reçues. Validez toujours req.body avant de traiter les données. Une validation robuste protège votre base de données et retourne des messages d'erreur utiles à l'utilisateur.

Validation inline — champs requis

router.post('/books', (req, res) => {
  const { title, author, genre, price } = req.body;
  const errors = [];

  // Champs requis
  if (!title  || title.trim().length === 0)
    errors.push({ field: 'title',  message: 'Le titre est requis' });
  if (!author || author.trim().length === 0)
    errors.push({ field: 'author', message: "L'auteur est requis" });
  if (!genre)
    errors.push({ field: 'genre',  message: 'Le genre est requis' });

  // Validation email (si applicable)
  // const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  // if (!emailRe.test(email)) errors.push({ field: 'email', message: 'Email invalide' });

  // Validation numérique
  if (price !== undefined) {
    if (isNaN(+price))
      errors.push({ field: 'price', message: 'Doit être un nombre' });
    else if (+price < 0)
      errors.push({ field: 'price', message: 'Doit être positif ou nul' });
  }

  // Validation longueur
  if (title && title.length > 200)
    errors.push({ field: 'title', message: 'Maximum 200 caractères' });

  if (errors.length > 0)
    return res.status(400).json({ success: false, error: 'Données invalides', details: errors });

  const book = { id: nextId++, title: title.trim(), author: author.trim(), genre, price: +price || 0 };
  books.push(book);
  res.status(201).json({ success: true, data: book });
});

Middleware de validation réutilisable — pattern validate(schema)

// middleware/validate.js — Middleware générique de validation

function validate(schema) {
  // Retourne un middleware Express
  return (req, res, next) => {
    const errors = [];
    const body   = req.body || {};

    for (const [field, rules] of Object.entries(schema)) {
      const value = body[field];

      if (rules.required && (value === undefined || value === null || value === ''))
        errors.push({ field, message: `${field} est requis` });

      if (value !== undefined) {
        if (rules.type === 'number' && isNaN(+value))
          errors.push({ field, message: `${field} doit être un nombre` });

        if (rules.min !== undefined && +value < rules.min)
          errors.push({ field, message: `${field} doit être >= ${rules.min}` });

        if (rules.maxLength && String(value).length > rules.maxLength)
          errors.push({ field, message: `${field} : maximum ${rules.maxLength} caractères` });

        if (rules.regex && !rules.regex.test(value))
          errors.push({ field, message: rules.regexMsg || `${field} invalide` });
      }
    }

    if (errors.length > 0)
      return res.status(400).json({ success: false, error: 'Validation échouée', details: errors });

    next(); // Validation OK — passer au handler
  };
}

module.exports = validate;

// ─────────────────────────────────────────────
// Utilisation dans les routes :
const validate = require('./middleware/validate');

const bookSchema = {
  title:  { required: true, maxLength: 200 },
  author: { required: true },
  genre:  { required: true },
  price:  { type: 'number', min: 0 }
};

// Le middleware validate(bookSchema) s'exécute AVANT le handler
router.post('/', validate(bookSchema), (req, res) => {
  // Si on arrive ici, req.body est valide
  const book = { id: nextId++, ...req.body };
  books.push(book);
  res.status(201).json({ success: true, data: book });
});

5. Pagination et Filtrage

Une API qui retourne potentiellement des milliers d'enregistrements doit implémenter la pagination. Les paramètres de query (?page=1&limit=10) contrôlent le découpage.

Pagination avec query params

// Fonction utilitaire de pagination
function paginate(data, page, limit) {
  const p     = Math.max(1, parseInt(page)  || 1);
  const l     = Math.min(100, Math.max(1, parseInt(limit) || 10));
  const total = data.length;
  const skip  = (p - 1) * l;
  const items = data.slice(skip, skip + l);

  return {
    data: items,
    meta: {
      total,
      page:       p,
      limit:      l,
      totalPages: Math.ceil(total / l),
      hasNext:    p < Math.ceil(total / l),
      hasPrev:    p > 1
    }
  };
}

// Utiliser dans une route GET
router.get('/', (req, res) => {
  const { page = 1, limit = 10 } = req.query;
  const result = paginate(books, page, limit);
  res.json({ success: true, ...result });
});
// GET /books?page=2&limit=5

Tri et filtres multiples

router.get('/books', (req, res) => {
  const { page = 1, limit = 10, sort = 'title', order = 'asc',
          genre, minPrice, maxPrice, year } = req.query;

  let data = [...books];

  // Filtrage
  if (genre)    data = data.filter(b => b.genre === genre);
  if (minPrice) data = data.filter(b => b.price >= +minPrice);
  if (maxPrice) data = data.filter(b => b.price <= +maxPrice);
  if (year)     data = data.filter(b => b.year  === +year);

  // Tri
  const validSorts = ['title', 'author', 'price', 'year', 'rating'];
  if (validSorts.includes(sort)) {
    data.sort((a, b) => {
      const av = a[sort], bv = b[sort];
      if (typeof av === 'string') return order === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
      return order === 'asc' ? av - bv : bv - av;
    });
  }

  // Pagination
  const result = paginate(data, page, limit);

  // Headers optionnels (standard RFC)
  res.setHeader('X-Total-Count', result.meta.total);
  res.setHeader('X-Page',        result.meta.page);
  res.setHeader('X-Limit',       result.meta.limit);

  res.json({ success: true, ...result });
});
// GET /books?genre=tech&minPrice=10&sort=price&order=asc&page=1&limit=5
Limitez toujours la valeur maximale de limit (ex: 100) pour éviter qu'un client malveillant ne demande des milliers d'entrées en une seule requête.
← Accueil Exercices →