Intermédiaire

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.

python
# 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érationMéthode HTTPDescription
CreatePOSTCréer une nouvelle ressource
ReadGETLire une ou plusieurs ressources
UpdatePUT / PATCHModifier une ressource existante
DeleteDELETESupprimer 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ègleBonMauvais
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

python
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
python
@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)