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)
- Client-Server : séparation UI / data
- Stateless : chaque requête contient tout le contexte
- Cacheable : les réponses sont implicitement/explicitement cacheables
- Uniform Interface : identifiant unique (URI), manipulation via representations, messages self-descriptive, HATEOAS
- Layered System : intermédiaires (proxy, cache, LB)
- 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éthode | Exemple | Avantage | Inconvénient |
|---|---|---|---|
| Path | /v1/users, /v2/users | Simple, explicite | Duplication code |
| Header | Accept: application/vnd.api+json;version=2 | Propre, URL propre | Moins visible |
| Query | /users?version=2 | Simple | Cache problématique |
| Subdomain | v2.api.example.com | Isolation totale | DNS, 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égie | Usage | Directives |
|---|---|---|
| Public CDN | Images, CSS, JS | public, max-age=31536000 |
| Private | Page HTML authentifiée | private, no-cache |
| No cache | API dynamique | no-cache, must-revalidate |
| Stale | API tolérante à la fraîcheur | public, max-age=60, stale-while-revalidate=600 |
| Conditional | API avec ETag | no-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)