MFormations
Modern Backend Engineering

Chapitre 6

Chapitre 06 — API REST

Chapitre 06 — API REST

Cours complet — API REST

1. Design REST (nommage, versioning)

Principes REST (Fielding, 2000)

  1. Client-Server : séparation UI / data
  2. Stateless : chaque requête contient tout le contexte
  3. Cacheable : les réponses sont implicitement/explicitement cacheables
  4. Uniform Interface : identifiant unique (URI), manipulation via representations, messages self-descriptive, HATEOAS
  5. Layered System : intermédiaires (proxy, cache, LB)
  6. Code on Demand (optionnel) : applets téléchargés

Nommage des ressources

Pluriel pour collections        : GET /users, POST /users
ID dans le path                 : GET /users/{id}, DELETE /users/{id}
Sous-ressources                 : GET /users/{id}/orders
Actions en sous-ressource       : POST /users/{id}/orders/{id}/cancel
Query parameters pour filtres   : GET /orders?status=pending

Règles de nommage :

  • Pas de verbes dans l'URL (utiliser les méthodes HTTP)
  • Snake_case ou kebab-case (selon conventions équipe)
  • Consistance : toujours pluriel, toujours ID
  • Pas de CRUD dans l'URL (pas de /createUser)
# Bon
GET /users
GET /users/42
POST /users
GET /users/42/orders
POST /users/42/orders
DELETE /orders/42

# Mauvais
GET /getUsers
POST /createUser
GET /users?id=42
POST /users/42/createOrder

Versioning

Stratégies :

MéthodeExempleAvantageInconvénient
Path/v1/users, /v2/usersSimple, expliciteDuplication code
HeaderAccept: application/vnd.api+json;version=2Propre, URL propreMoins visible
Query/users?version=2SimpleCache problématique
Subdomainv2.api.example.comIsolation totaleDNS, certificats

Recommandation : version dans le path pour les APIs publiques.

GET /v1/users/42
GET /v2/users/42  # Breaking changes seulement

Breaking changes (nécessitent une nouvelle version) :

  • Suppression d'un champ
  • Renommage d'un champ
  • Changement de type
  • Changement de comportement
  • Suppression d'endpoint

Non-breaking changes (pas de version) :

  • Ajout d'un champ
  • Ajout d'un endpoint
  • Ajout d'un paramètre optionnel

2. Pagination, filtering, sorting

Pagination

Offset-based (simple, pas performant pour grandes pages) :

GET /users?offset=0&limit=20
GET /users?page=1&per_page=20

Problème : si des données sont ajoutées/supprimées, les pages se décalent.

Cursor-based (stable, performant) :

GET /users?cursor=eyJpZCI6MTAwMH0&limit=20
Response:
{
  "data": [...],
  "pagination": {
    "cursor": "eyJpZCI6MTAwMH0",
    "has_more": true,
    "next_url": "/users?cursor=eyJpZCI6MTAwMH0&limit=20"
  }
}

Keyset pagination (équivalent SQL) :

SELECT * FROM users WHERE id > 1000 ORDER BY id LIMIT 20;

Filtering

# Simple (equals)
GET /products?category=electronics

# Multiple values
GET /products?category=electronics,books

# Range
GET /products?price=gte:100,lte:500

# Date
GET /orders?created_at=gte:2025-01-01

# Text search
GET /products?q=search+term

# Nested
GET /orders?filter[status]=pending&filter[total]=gte:50

# Complexe (JSON-encoded)
GET /products?filter={"category":"electronics","price":{"$gte":100}}

Sorting

# Simple
GET /users?sort=created_at

# Descending
GET /users?sort=-created_at

# Multiple
GET /users?sort=-created_at,name

# Complexe
GET /users?sort[]=created_at:desc&sort[]=name:asc

3. HATEOAS

Qu'est-ce que HATEOAS ?

Hypermedia As The Engine Of Application State Les réponses contiennent des liens qui guident le client dans l'API.

{
  "data": {
    "id": 42,
    "name": "Alice"
  },
  "_links": {
    "self": { "href": "/users/42" },
    "orders": { "href": "/users/42/orders" },
    "friends": { "href": "/users/42/friends" },
    "profile": { "href": "/users/42/profile" },
    "avatar": { "href": "/avatars/alice.jpg", "type": "image/jpeg" }
  }
}

Avantages

  • Client auto-découvrable (réduit la documentation nécessaire)
  • Évolution de l'API sans casser les clients
  • Navigation guidée (le client suit les liens)

Inconvénients

  • Payload plus lourd
  • Complexité implémentation
  • Peu utilisé en pratique

Niveaux de maturité REST (Richardson Maturity Model)

Level 0 : RPC (POST /getUser)
Level 1 : Resources (POST /users, GET /users/42)
Level 2 : HTTP Verbs (GET, POST, PUT, DELETE)
Level 3 : HATEOAS (liens dans les réponses)

4. Idempotence

Propriété

Une opération idempotente peut être appliquée N fois avec le même résultat qu'une seule fois.

GET /users/42      → Idempotent (safe)
PUT /users/42      → Idempotent (même body = même résultat)
DELETE /users/42   → Idempotent (après DELETE, retourne 404 ou 204)
POST /users        → Non idempotent (crée une nouvelle ressource)
PATCH /users/42    → Non idempotent (sauf si remplacement complet)

Idempotence key (POST idempotent)

POST /payments
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000

{
  "amount": 100,
  "currency": "EUR"
}
  • Le serveur vérifie si la clé a déjà été traitée
  • Si oui, retourne le résultat précédent (sans traiter)
  • Stratégies : stocker 24h, cleanup périodique

Cas pratique : Stripe Idempotency

# Stripe utilise Idempotency-Key pour les paiements
# Si le client timeout, il peut réessayer avec la même clé
# Stripe garantit que le paiement ne sera traité qu'une fois

5. Caching (ETag, Cache-Control)

ETag (Entity Tag)

# Requête
GET /users/42
Response:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# Requête suivante (conditionnelle)
GET /users/42
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# Réponse si non modifié
304 Not Modified

Cache-Control

# Cache public (CDN, proxy)
Cache-Control: public, max-age=3600, s-maxage=86400

# Cache privé (navigateur seulement)
Cache-Control: private, max-age=600

# Pas de cache
Cache-Control: no-cache, no-store, must-revalidate

# Stale-while-revalidate (servir version périmée + rafraîchir)
Cache-Control: public, max-age=3600, stale-while-revalidate=86400

# Immutable (ne jamais revalider)
Cache-Control: public, max-age=31536000, immutable

Stratégies de caching

StratégieUsageDirectives
Public CDNImages, CSS, JSpublic, max-age=31536000
PrivatePage HTML authentifiéeprivate, no-cache
No cacheAPI dynamiqueno-cache, must-revalidate
StaleAPI tolérante à la fraîcheurpublic, max-age=60, stale-while-revalidate=600
ConditionalAPI avec ETagno-cache (mais ETag)

Cache Invalidation

  • Time-based (TTL) : le cache expire après N secondes
  • Event-based : purge sur modification (Redis pub/sub)
  • Pattern-based : invalidation par préfixe
  • Surrogate keys (CDN Fastly) : tags associés aux objets

6. OpenAPI / Swagger

Structure OpenAPI 3.1

openapi: 3.1.0
info:
  title: Users API
  version: 1.0.0
  description: API de gestion des utilisateurs
servers:
  - url: https://api.example.com/v1
paths:
  /users:
    get:
      summary: Liste des utilisateurs
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20 }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Succès
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
    post:
      summary: Créer un utilisateur
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUser'
      responses:
        '201':
          description: Créé
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        email: { type: string, format: email }
        created_at: { type: string, format: date-time }
    CreateUser:
      type: object
      required: [name, email]
      properties:
        name: { type: string, minLength: 2 }
        email: { type: string, format: email }
    Pagination:
      type: object
      properties:
        cursor: { type: string, nullable: true }
        has_more: { type: boolean }

Génération automatique

# FastAPI génère OpenAPI automatiquement
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    id: int
    name: str
    email: str

@app.get("/users", response_model=list[User])
async def list_users():
    pass  # OpenAPI généré automatiquement

7. Error Handling

Structure d'erreur standard

// RFC 7807 Problem Details
{
  "type": "https://api.example.com/errors/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "The request body contains invalid fields",
  "instance": "/users",
  "errors": [
    {
      "field": "email",
      "message": "Invalid email format",
      "code": "INVALID_FORMAT"
    },
    {
      "field": "name",
      "message": "Name is required",
      "code": "REQUIRED_FIELD"
    }
  ]
}

Error codes

HTTP/1.1 400 Bad Request
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request parameters",
    "details": [
      { "field": "email", "reason": "must be a valid email", "code": "INVALID_EMAIL" }
    ]
  }
}

HTTP/1.1 401 Unauthorized
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication required",
    "details": "Missing or invalid Authorization header"
  }
}

HTTP/1.1 403 Forbidden
{
  "error": {
    "code": "FORBIDDEN",
    "message": "Insufficient permissions",
    "details": "User does not have 'admin' role"
  }
}

HTTP/1.1 404 Not Found
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found",
    "details": "User with id 42 not found"
  }
}

HTTP/1.1 409 Conflict
{
  "error": {
    "code": "CONFLICT",
    "message": "Resource already exists",
    "details": "A user with email 'alice@test.com' already exists"
  }
}

HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      { "field": "age", "reason": "must be >= 0 and <= 150" }
    ]
  }
}

HTTP/1.1 429 Too Many Requests
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests",
    "details": "Rate limit exceeded. Try again in 30 seconds."
  }
}

8. Status Codes

Par catégorie

# 2xx Success
200 OK                    → GET, PUT, PATCH réussis
201 Created               → POST réussi (ressource créée)
202 Accepted              → Requête acceptée (traitement asynchrone)
204 No Content            → DELETE réussi, pas de body

# 3xx Redirection
301 Moved Permanently     → Ressource déplacée
302 Found                 → Redirection temporaire
304 Not Modified          → Cache hit (ETag/If-None-Match)

# 4xx Client Error
400 Bad Request           → Requête invalide (malformée)
401 Unauthorized          → Authentification manquante/invalide
403 Forbidden             → Permissions insuffisantes
404 Not Found             → Ressource inexistante
405 Method Not Allowed    → Méthode non supportée
406 Not Acceptable        → Content-Type demandé non disponible
409 Conflict              → Conflit (doublon, état incohérent)
410 Gone                  → Ressource supprimée définitivement
415 Unsupported Media Type → Content-Type envoyé non supporté
422 Unprocessable Entity  → Validation échouée
429 Too Many Requests     → Rate limit dépassé

# 5xx Server Error
500 Internal Server Error → Erreur générique
502 Bad Gateway           → Proxy ne peut pas joindre le backend
503 Service Unavailable   → Service temporairement indisponible
504 Gateway Timeout       → Timeout du backend

Références

  • RESTful Web APIs (Richardson & Amundsen)
  • OpenAPI Specification (openapis.org)
  • RFC 7231 (HTTP semantics), RFC 7807 (Problem Details)
  • Microsoft REST API Guidelines (github.com/microsoft/api-guidelines)
  • Google API Design Guide (cloud.google.com/apis/design)