Aller au contenu

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
  • type doit être une URI stable et documentee

Stratégies de versioning

URL versioning (le plus courant)

/v1/orders
/v2/orders

Avantages : visible, cacheable, facile à router. Inconvénients : duplication de routes, tendency a copier-coller.

Header versioning

GET /orders
Accept: application/vnd.myapi.v2+json

Avantages : URL propre, semantiquement correct (c'est la représentation qui change). Inconvénients : difficile à tester dans un navigateur, caching complexe.

Query parameter

GET /orders?version=2

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.