API Design¶
Concevoir des interfaces qui restent stables, coherentes et evoluent sans casser les clients existants.
Pourquoi l'API Design est critique¶
Une API mal conçue vous suivra pendant des années. Renommer un champ, changer un code HTTP ou modifier une structure de réponse casse tous les clients existants. La conception doit être intentionnelle des le début.
Une API est un contrat. Comme un contrat legal, le modifier a des conséquences.
Principes fondamentaux¶
Cohérence : même nommage, même structure d'erreur, même pagination partout. Un développeur qui connait un endpoint doit pouvoir deviner les autres.
Backward compatibility : ajouter est toujours safe. Supprimer ou modifier casse. Planifiez les suppressions avec des periodes de deprecation.
Design first : ecrivez le contrat (OpenAPI, proto) avant le code. Faites le valider par les consommateurs.
Conventions REST¶
Nommage des ressources¶
# Bon — noms au pluriel, hierarchie claire
GET /orders
GET /orders/{id}
POST /orders
PATCH /orders/{id}
DELETE /orders/{id}
GET /orders/{id}/items
POST /orders/{id}/items
# Mauvais — verbes dans l'URL, singuliier inconsistant
GET /getOrder/{id}
POST /createOrder
GET /order/{id}/getItems
Codes HTTP¶
| Code | Signification | Quand l'utiliser |
|---|---|---|
| 200 | OK | GET, PATCH, PUT réussis |
| 201 | Created | POST qui crée une ressource |
| 204 | No Content | DELETE réussi, ou PATCH sans corps de réponse |
| 400 | Bad Request | Validation échouée, données malformees |
| 401 | Unauthorized | Non authentifie |
| 403 | Forbidden | Authentifie mais pas autorise |
| 404 | Not Found | Ressource inexistante |
| 409 | Conflict | État incompatible (doublon, version) |
| 422 | Unprocessable Entity | Semantiquement incorrect (métier) |
| 429 | Too Many Requests | Rate limiting |
| 500 | Internal Server Error | Erreur non anticipee côté serveur |
Pagination¶
GET /orders?page=2&per_page=20
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 20,
"total": 243,
"total_pages": 13
}
}
Pour les grandes collections, preferez la pagination par curseur :
GET /orders?cursor=eyJpZCI6MTAwfQ&limit=20
{
"data": [...],
"next_cursor": "eyJpZCI6MTIwfQ",
"has_more": true
}
Cursor vs offset
La pagination par offset (page=2) pose problème si des éléments sont inseres ou supprimes entre deux requêtes. La pagination par curseur est stable et plus performante sur les grandes tables.
Gestion des erreurs — RFC 7807¶
Le standard RFC 7807 Problem Details définit un format d'erreur JSON universel :
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/insufficient-stock",
"title": "Stock insuffisant",
"status": 422,
"detail": "Le produit SKU-42 n'a que 3 unites en stock, 10 demandees.",
"instance": "/orders/ORD-789",
"extensions": {
"product_id": "SKU-42",
"available": 3,
"requested": 10
}
}
from dataclasses import dataclass
@dataclass
class ProblemDetail:
type: str
title: str
status: int
detail: str
instance: str | None = None
extensions: dict | None = None
def to_dict(self) -> dict:
result = {
"type": self.type,
"title": self.title,
"status": self.status,
"detail": self.detail,
}
if self.instance:
result["instance"] = self.instance
if self.extensions:
result.update(self.extensions)
return result
Règles :
- Ne jamais exposer les stack traces en production
- Toujours retourner le même format d'erreur, quel que soit l'endpoint
typedoit être une URI stable et documentee
Stratégies de versioning¶
URL versioning (le plus courant)¶
Avantages : visible, cacheable, facile à router. Inconvénients : duplication de routes, tendency a copier-coller.
Header versioning¶
Avantages : URL propre, semantiquement correct (c'est la représentation qui change). Inconvénients : difficile à tester dans un navigateur, caching complexe.
Query parameter¶
Evitez : non standard, pollue les URLs, caching problemmatique.
Backward compatibility en pratique¶
# v1 — champ "name" simple
{"id": "USR-1", "name": "Alice Martin"}
# v2 — decompose en prenom/nom
# MAUVAIS : supprimer "name" casse les clients v1
{"id": "USR-1", "first_name": "Alice", "last_name": "Martin"}
# BON : ajouter les nouveaux champs, deprecer l'ancien
{
"id": "USR-1",
"name": "Alice Martin", # deprecated, sera supprime en v3
"first_name": "Alice",
"last_name": "Martin"
}
Documentez les depreciations avec un header :
Deprecation: true
Sunset: Sat, 01 Jan 2026 00:00:00 GMT
Link: <https://api.example.com/v3/users>; rel="successor-version"
Tableau des décisions de design¶
| Décision | Option A | Option B | Recommandation |
|---|---|---|---|
| Versioning | URL /v1/ | Header Accept | URL pour les API publiques |
| Pagination | Offset | Curseur | Curseur pour collections larges |
| Format erreur | Custom JSON | RFC 7807 | RFC 7807 |
| Nommage champs | camelCase | snake_case | Cohérent avec votre stack (snake pour Python/Ruby) |
| Dates | Timestamp Unix | ISO 8601 | ISO 8601 (2024-01-15T10:30:00Z) |
| IDs | Integer séquentiel | UUID | UUID (evite l'énumération) |
N'exposez pas votre modèle de base de données
Une API qui reflete exactement vos tables SQL va vous pieger. Chaque refactoring de schéma devient une breaking change. Designez vos ressources API comme un vocabulaire métier, pas comme un miroir de votre persistence.