Conventions API REST
Standards pratiques pour le design, l’implémentation et le débogage d’APIs HTTP.
Nommage des URL
Les ressources se placent au pluriel, en kebab-case. Les sous-ressources utilisent des slashs.
| Pattern | Exemple | Sémantique |
|---|---|---|
| Ressource | GET /users | Lister tous les utilisateurs |
| Ressource unique | GET /users/42 | Récupérer l’utilisateur 42 |
| Sous-ressource | GET /users/42/posts | Posts de l’utilisateur 42 |
| Collection | GET /users/42/friends | Liste des amis |
BON : /users/42/posts/15
MAUVAIS : /Users/42/GetPosts/15
MAUVAIS : /user/42/post/15 (singulier, CamelCase)
MAUVAIS : /getUsers/42/posts/15 (verbe dans l’URL)
Règle d’or : l’URL décrit quoi, pas faire. Le verbe se trouve dans la méthode HTTP.
Méthodes HTTP
| Méthode | Sécurité | Idempotence | Usage |
|---|---|---|---|
GET | Oui (ne modifie pas) | Oui | Lecture seule |
HEAD | Oui | Oui | Headers uniquement (vérification) |
POST | Non | Non | Création de ressource |
PUT | Non | Oui | Remplacement complet |
PATCH | Non | Non | Mise à jour partielle |
DELETE | Non | Oui | Suppression |
OPTIONS | Oui | Oui | Découvrir les méthodes autorisées |
PUT vs PATCH — Quand utiliser lequel
// PUT : on remplace TOUT l'utilisateur
fetch('/api/users/42', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Arthur',
email: 'arthur@example.com',
role: 'admin',
avatar: 'https://...',
// Tous les champs doivent être fournis
}),
});
// PATCH : on ne modifie que certains champs
fetch('/api/users/42', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ avatar: 'https://...' }),
});| Critère | PUT | PATCH |
|---|---|---|
| Champs manquants | Considérés comme null (remplacés) | Ignorés (conservés) |
| Idempotence | Oui — plusieurs PUT = même résultat | Non — plusieurs PATCH peuvent différer |
| Cas d’usage | Configuration, préférences complètes | Modifications ciblées |
Codes de statut
Succès — 2xx
| Code | Signification | Quand |
|---|---|---|
200 OK | Requête réussie | GET, PUT, PATCH, DELETE |
201 Created | Ressource créée | POST réussi |
204 No Content | Succès, sans corps | DELETE réussi |
Redirections — 3xx
| Code | Signification | Quand |
|---|---|---|
301 Moved Permanently | URL définitivement changée | Renommage permanent de ressource |
302 Found | Redirection temporaire | Migration en cours |
304 Not Modified | Cache valide | If-None-Match / ETag |
Erreurs client — 4xx
| Code | Signification | Quand |
|---|---|---|
400 Bad Request | Requête invalide | JSON malformé, paramètre manquant |
401 Unauthorized | Authentification requise | Token absent ou invalide |
403 Forbidden | Non autorisé | Rôle insuffisant |
404 Not Found | Ressource introuvable | ID inexistant |
405 Method Not Allowed | Méthode interdite | Mauvaise méthode HTTP |
409 Conflict | Conflit | Duplicate (ex. email existant) |
422 Unprocessable Entity | Sémantique invalide | Validation métier échouée |
429 Too Many Requests | Rate limit dépassé | Trop de requêtes |
Erreurs serveur — 5xx
| Code | Signification | Quand |
|---|---|---|
500 Internal Server Error | Erreur générique | Bugs, exceptions non gérées |
502 Bad Gateway | Passerelle invalide | Service aval hors-ligne |
503 Service Unavailable | Indisponibilité | Maintenance, surcharge |
Piège :
400vs422. Un400concerne la syntaxe (JSON invalide, type incorrect). Un422concerne la sémantique (champs valides syntaxiquement mais interdits par les règles métier).
Headers essentiels
| En-tête | Client → Serveur | Serveur → Client | Usage |
|---|---|---|---|
Content-Type | Oui | — | Type du payload (application/json) |
Accept | Oui | — | Type attendu de la réponse |
Authorization | Oui | — | Token (Bearer <token>) |
X-Request-ID | Oui | — | Corrélation des logs |
X-RateLimit-Limit | — | Oui | Limite de requêtes par fenêtre |
X-RateLimit-Remaining | — | Oui | Requêtes restantes |
X-RateLimit-Reset | — | Oui | Timestamp de reset (Unix) |
ETag | — | Oui | Version de la ressource (cache) |
If-None-Match | Oui | — | Vérifier la mise à jour |
Cache-Control | — | Oui | Stratégie de cache (max-age, no-store) |
Link | — | Oui | Pagination (relation next, prev) |
Retry-After | — | Oui | Délai avant nouvelle tentative |
Exemple de headers en production :
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "a1b2c3d4"
X-Request-ID: req-7f8a9b0c
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1720000000
Cache-Control: public, max-age=60
Link: <https://api.exemple.com/users?page=2>; rel="next"Pagination
Offset (numérique)
Simple, adapté aux petites API. Inefficace pour les gros volumes (décalage des résultats lors de suppressions).
GET /api/users?offset=0&limit=20
GET /api/users?offset=20&limit=20{
"data": [ ... ],
"pagination": {
"offset": 0,
"limit": 20,
"total": 150,
"hasMore": true
}
}Cursor (position)
Recommandé pour les APIs avec volumes importants. Résiste aux modifications concurrentes.
GET /api/users?cursor=eyJpZCI6NDJ9&limit=20{
"data": [ ... ],
"pagination": {
"limit": 20,
"nextCursor": "eyJpZCI6NjJ9",
"prevCursor": "eyJpZCI6MjJ9"
}
}Piège : avec la pagination offset, les suppressions entre deux pages peuvent faire « sauter » des éléments ou les dupliquer. Pour les feeds et timelines, toujours utiliser cursor.
Headers Link (RFC 5988)
Pour les APIs nécessitant une navigation puissante, renvoyer les liens de pagination dans un en-tête Link :
Link: <https://api.exemple.com/users?page=1>; rel="first",
<https://api.exemple.com/users?page=2>; rel="next",
<https://api.exemple.com/users?page=8>; rel="last"Format des erreurs
Une réponse d’erreur standardisée facilite le traitement côté client.
{
"error": {
"code": "VALIDATION_ERROR",
"status": 422,
"message": "La validation a échoué.",
"details": [
{
"field": "email",
"message": "Cet email est déjà utilisé.",
"rule": "unique"
},
{
"field": "password",
"message": "Minimum 8 caractères requis.",
"rule": "min_length"
}
],
"requestId": "req-7f8a9b0c"
}
}Champs recommandés :
| Champ | Requis | Description |
|---|---|---|
error.code | Oui | Clé machine lisible (ex. VALIDATION_ERROR, NOT_FOUND) |
error.status | Oui | Code HTTP correspondant |
error.message | Oui | Message lisible par un humain |
error.details[] | Non | Erreurs fines par champ (validations) |
error.requestId | Oui | ID de corrélation pour le support |
// Traitement côté client
async function apiCall(url, opts = {}) {
const res = await fetch(url, opts);
if (!res.ok) {
const body = await res.json();
const err = body.error || {
code: 'UNKNOWN',
status: res.status,
message: res.statusText,
};
// Exemple de mapping par code
switch (err.code) {
case 'UNAUTHORIZED':
// Rediriger vers la page de connexion
window.location.href = '/login';
break;
case 'RATE_LIMITED':
// Attendre le retry
const retryAfter = res.headers.get('Retry-After');
setTimeout(() => apiCall(url, opts), retryAfter * 1000);
return;
}
throw new APIError(err);
}
return res.json();
}Filtrage et tri
Requête simple
GET /api/posts?status=published&author=42&created_after=2025-01-01- Paramètres avec valeur unique :
?status=published - Paramètres multiples :
?role=admin&role=editor(tableau) ou?role[]=admin&role[]=editor - Filtres sur relations :
?author.name=Arthur(dot notation)
Tri
GET /api/posts?sort=-published_at,+title-= ordre décroissant+ou rien = ordre croissant- Plusieurs champs : virgule
Recherche texte
GET /api/posts?q=recherche+en+français&page=1Piège : toujours sanitizer les paramètres côté serveur.
?q=<script>alert(1)</script>ne doit jamais passer en clair au SGBD sans échappement.
Versioning
Choisir une stratégie tôt, avant le premier release public.
| Stratégie | Exemple | Avantages | Inconvénients |
|---|---|---|---|
| Header | Accept: application/vnd.api.v2+json | URLs propres, backward compatible | Moins visible, besoin de doc |
| URL path | /api/v2/users | Visible, facile à comprendre | URLs qui vieillissent mal |
| Query param | /api/users?version=2 | Simple | Moins standard |
Recommandé : header
Acceptpour les APIs publiques, path/v2/pour les APIs internes.
Checklist de design API
- URL au pluriel, kebab-case, pas de verbes
- Méthode HTTP correspond à la sémantique (PUT vs PATCH)
- Code de statut pertinent (4xx pour client, 5xx pour serveur)
- Format d’erreur standardisé avec
requestId - Pagination offset ou cursor (pas les deux)
- Headers
X-RateLimit-*pour informer le client -
ETagpour le cache côté client - Stratégie de versioning choisie et documentée