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éthode | Action | URL exemple | Status réussite | Idempotent ? |
|---|---|---|---|---|
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
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
| Code | Nom | Quand l'utiliser |
|---|---|---|
200 | OK | GET / PUT / PATCH réussi |
201 | Created | POST réussi — ressource créée |
204 | No Content | DELETE réussi — pas de corps de réponse |
400 | Bad Request | Données invalides, champs manquants |
401 | Unauthorized | Non authentifié — token manquant/expiré |
403 | Forbidden | Authentifié mais non autorisé |
404 | Not Found | Ressource introuvable |
409 | Conflict | Doublon — email déjà utilisé |
422 | Unprocessable Entity | Syntaxe OK mais données sémantiquement invalides |
500 | Internal Server Error | Erreur 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 !
});
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');
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
limit (ex: 100) pour éviter qu'un client malveillant ne demande des milliers d'entrées en une seule requête.