API REST
REST (Representational State Transfer) est l'architecture d'API la plus utilisée sur le web. Ce chapitre détaille ses caractéristiques, le pattern CRUD et les conventions de design d'URI.
Caractéristiques de REST
Une API est dite RESTful lorsqu'elle respecte les principes suivants :
1. Stateless (sans état)
Chaque requête contient toutes les informations nécessaires à son traitement. Le serveur ne conserve aucun état de session entre deux requêtes.
# Chaque requête porte son propre token d'authentification
@books_bp.route('', methods=['GET'])
def get_books():
# Le token est dans le header, pas dans une session serveur
token = request.headers.get('Authorization')
user = verify_token(token)
books = book_service.find_all_for_user(user.id)
return jsonify(books), HTTPStatus.OK
Pourquoi stateless ?
Le stateless facilite la scalabilité : chaque requête peut être traitée par n'importe quel serveur, ce qui permet de répartir la charge sans se soucier de l'état de session.
2. Interface uniforme
Les ressources sont accessibles via des conventions prévisibles. Un développeur qui connaît une API REST peut en deviner la structure.
3. Indépendance client/serveur
Le client et le serveur évoluent indépendamment. Le frontend peut être réécrit sans toucher au backend, et vice versa, tant que le contrat d'API est respecté.
4. Cachable
Les réponses peuvent être mises en cache pour améliorer les performances (comme vu dans le chapitre précédent).
5. Code à la demande (optionnel)
Le serveur peut fournir du code exécutable au client (JavaScript, par exemple). Ce principe est rarement utilisé dans la pratique.
CRUD et méthodes HTTP
Le pattern CRUD (Create, Read, Update, Delete) est le fondement de la plupart des API REST :
| Opération | Méthode HTTP | Description |
|---|---|---|
| Create | POST | Créer une nouvelle ressource |
| Read | GET | Lire une ou plusieurs ressources |
| Update | PUT / PATCH | Modifier une ressource existante |
| Delete | DELETE | Supprimer une ressource |
Quelle est la différence entre PUT et PATCH ?
Design d'URI
L'objectif est d'avoir des URL uniformes et prévisibles pour toutes les ressources.
Conventions
| Règle | Bon | Mauvais |
|---|---|---|
| Noms de ressources au pluriel | /books | /book |
| Utiliser des noms, pas des verbes | /books | /getBooks |
| Minuscules et tirets | /order-items | /OrderItems |
| Hiérarchie logique | /authors/5/books | /getBooksByAuthor?id=5 |
CRUD complet pour une ressource "livres"
GET /api/books → Liste de tous les livres
GET /api/books/:id → Détail d'un livre
POST /api/books → Créer un livre
PUT /api/books/:id → Remplacer un livre
PATCH /api/books/:id → Modifier partiellement un livre
DELETE /api/books/:id → Supprimer un livre
Implémentation Flask complète
from flask import Flask, Blueprint, request, jsonify
from http import HTTPStatus
from pydantic import BaseModel, ValidationError
from typing import Optional
# --- DTOs (Pydantic) ---
class CreateBookDTO(BaseModel):
title: str
author: str
year: int
isbn: Optional[str] = None
class UpdateBookDTO(BaseModel):
title: Optional[str] = None
author: Optional[str] = None
year: Optional[int] = None
isbn: Optional[str] = None
# --- Routes (Blueprint) ---
books_bp = Blueprint('books', __name__, url_prefix='/api/books')
@books_bp.route('', methods=['GET'])
def get_books():
"""Liste paginée de livres"""
offset = request.args.get('offset', default=0, type=int)
limit = request.args.get('limit', default=20, type=int)
books = book_service.find_all(offset=offset, limit=limit)
return jsonify(books), HTTPStatus.OK
@books_bp.route('/<int:book_id>', methods=['GET'])
def get_book(book_id: int):
"""Détail d'un livre"""
book = book_service.get_by_id(book_id)
if not book:
return jsonify({"error": "Livre non trouvé"}), HTTPStatus.NOT_FOUND
return jsonify(book), HTTPStatus.OK
@books_bp.route('', methods=['POST'])
def create_book():
"""Créer un livre"""
data = request.get_json()
try:
dto = CreateBookDTO(**data)
except ValidationError as e:
return jsonify({"error": e.errors()}), HTTPStatus.BAD_REQUEST
new_book = book_service.create(dto)
return jsonify(new_book), HTTPStatus.CREATED
@books_bp.route('/<int:book_id>', methods=['PUT'])
def replace_book(book_id: int):
"""Remplacer intégralement un livre"""
data = request.get_json()
try:
dto = CreateBookDTO(**data) # PUT exige tous les champs
except ValidationError as e:
return jsonify({"error": e.errors()}), HTTPStatus.BAD_REQUEST
updated = book_service.replace(book_id, dto)
if not updated:
return jsonify({"error": "Livre non trouvé"}), HTTPStatus.NOT_FOUND
return jsonify(updated), HTTPStatus.OK
@books_bp.route('/<int:book_id>', methods=['PATCH'])
def update_book(book_id: int):
"""Modifier partiellement un livre"""
data = request.get_json()
try:
dto = UpdateBookDTO(**data) # PATCH : champs optionnels
except ValidationError as e:
return jsonify({"error": e.errors()}), HTTPStatus.BAD_REQUEST
updated = book_service.update(book_id, dto)
if not updated:
return jsonify({"error": "Livre non trouvé"}), HTTPStatus.NOT_FOUND
return jsonify(updated), HTTPStatus.OK
@books_bp.route('/<int:book_id>', methods=['DELETE'])
def delete_book(book_id: int):
"""Supprimer un livre"""
deleted = book_service.delete(book_id)
if not deleted:
return jsonify({"error": "Livre non trouvé"}), HTTPStatus.NOT_FOUND
return '', HTTPStatus.NO_CONTENT
Ressources imbriquées
Pour les relations entre ressources, on utilise une hiérarchie dans l'URL :
GET /api/authors/5/books → Livres de l'auteur 5
POST /api/authors/5/books → Créer un livre pour l'auteur 5
GET /api/orders/12/items → Articles de la commande 12
@authors_bp.route('/<int:author_id>/books', methods=['GET'])
def get_author_books(author_id: int):
"""Livres d'un auteur spécifique"""
books = book_service.find_by_author(author_id)
return jsonify(books), HTTPStatus.OK
Quelle URI est la plus conforme aux conventions REST ?
À retenir
Points clés
- Une API REST est stateless : chaque requête est autonome
- Le pattern CRUD mappe directement sur les méthodes HTTP (POST, GET, PUT/PATCH, DELETE)
- PUT remplace intégralement, PATCH modifie partiellement
- Les URI doivent être prévisibles : noms au pluriel, pas de verbes, hiérarchie logique
- Utilisez Pydantic pour valider les DTOs côté Flask
- Les ressources imbriquées se traduisent par des hiérarchies d'URL (
/authors/5/books)