12 Application Programming Interfaces
If you don’t know what an API should look like, write the tests first as it’ll force you to think of the “customer” which in this case is you. You’ll invariably discover cases that you would not have thought of if you had just written the code first and tests after.
Zarar Siddiqi (Good Software Development Habits 2024)
12.1 REST
Le style d’architecture REST (Representational State Transfer) repose sur l’utilisation du protocole HTTP comme interface uniforme pour accéder et manipuler les ressources d’une application. C’est un standard largement adopté pour la conception d’API web.
12.1.1 Principes fondamentaux
Une API REST bien conçue suit les principes suivants :
- Ressources identifiables par des URLs
12.2 [source,bash]
GET /users/42 PATCH /orders/1234 —-
- Utilisation appropriée des verbes HTTP
- GET: récupération
- POST: création
- PUT/PATCH: mise à jour
- DELETE: suppression
- Stateless : chaque requête contient toutes les informations nécessaires
- Représentation : les ressources sont représentées généralement en JSON, mais pourraient l’être dans un autre format simplement en réorientant la sortie.
12.2.1 Exemple minimal avec Django Rest Framework (DRF)
Nous pourrions nous passer d’un framework cite:[emma_django_rest], mais le protocol REST va plus loin que “juste” offrir des données structurées en JSON. REST touche également à l’authentification, aux permissions, à l’extensibilité, … Dans cet exemple, nous nous contenterons de Django-REST-framework, parce qu’il agit un peu comme standard, malgré sa complexité sous-jacente.
# serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
# views.py
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
# urls.py
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]La syntaxe et la structure de Django Rest Framework est devenu (de facto) un standard pour les développeurs. Ce framework reprend un large écosystème d’outils (caching HTTP, clients, documentation automatique et exploratoire, …) et présente une séparation claire des responsabilités, entre ressources et représentations.
12.2.2 Limites
- Sous-utilisation du protocole HTTP : trop d’APIs ignorent les codes de statut, les headers (ETag, * If-Modified-Since…), ou la négociation de contenu.
- Sous-division des endpoints : une API REST trop fragmentée peut entraîner de nombreuses requêtes (problème * classique du mobile).
- Couplage fort au modèle de données : les ressources exposées sont souvent une simple projection du modèle * Django, sans encapsulation.
- Couplage au modèle Django : logique métier directement encodée dans les ModelSerializer et ViewSet, difficile à * tester ou réutiliser hors contexte HTTP.
- Manque de séparation des couches : peu d’espace pour insérer des services ou des règles métiers explicites * entre les vues et les modèles.
- Tests difficiles : la sur-abstraction de DRF peut compliquer les tests unitaires (fort besoin de mocks, * complexité de setup).
- Extensibilité coûteuse : override des comportements internes de DRF = complexité exponentielle.
- Convention sur configuration… sauf quand il faut casser les conventions.
12.2.3 Bonnes pratiques
- Regrouper les routes par ressource, non par action.
- Toujours paginer les listes.
- Fournir des filtres, tris et recherches via query parameters.
- Séparer clairement les endpoints publics, internes et admin.
- Documenter l’API via OpenAPI / Swagger.
12.2.4 Cas d’usage recommandé
REST est adapté aux cas suivants :
- CRUD simple sur des objets métier.
- APIs consommées par des services internes.
- Frontends SPA classiques (avec React, Vue, etc.).
12.2.5 Exemple minimal avec Django Ninja
https://django-ninja.dev/
12.3 Pourquoi migrer vers FastAPI
En prenant DRF comme exemple pratique, nous remarquons qu’il est extrêmement basé sur Django (comme son nom l’indique) - ce qui implique qu’une réécriture du coeur de l’application nécessitera en plus une réécriture de toutes ses dépendances.
FastAPI autorise une découpe beaucoup plus propre des différentes couches applicatives. Si DRF utilise le modèle, sérialiseur et vue ci-dessous :
# models.py
class User(models.Model):
email = models.EmailField(unique=True)
name = models.CharField(max_length=255)
# serializer.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['email', 'name']
# views.py
class UserCreateView(generics.CreateAPIView):
serializer_class = UserSerializer
queryset = User.objects.all()Nous pouvons constater que la logique métier est souvent placée dans les sérialiseurs - qui devraient en fait se concentrer sur … la sérialisation des données plutôt que sur ajouter de l’intelligence dans ses traitements.
L’équivalent pour FastAPI ressemblerait plutôt à ceci, en suivant une Clean Architecture :
12.4 [source,text]
users/ ├── domain/ │ └── entities.py ├── application/ │ └── create_user.py ├── infrastructure/ │ └── repositories.py └── presentation/ └── api.py —-
Et plus spécifiquement :
# domain/entities.py
from pydantic import BaseModel, EmailStr
class User(BaseModel):
email: EmailStr
name: str
# application/create_user.py
from domain.entities import User
def create_user(data: User, user_repo) -> User:
if user_repo.email_exists(data.email):
raise ValueError("Email already in use.")
return user_repo.save(data)
# infrastructure/repositories.py
from domain.entities import User
class DjangoUserRepo:
def email_exists(self, email: str) -> bool:
return UserModel.objects.filter(email=email).exists()
def save(self, data: User) -> User:
instance = UserModel.objects.create(**data.dict())
return User.from_orm(instance)
# presentation/api.py
from fastapi import APIRouter, Depends, HTTPException
from application.create_user import create_user
from infrastructure.repositories import DjangoUserRepo
from domain.entities import User
router = APIRouter()
@router.post("/users", response_model=User)
def register_user(user: User, repo: DjangoUserRepo = Depends()):
try:
return create_user(user, repo)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))- Injection de dépendances native : séparation claire des services, contrôleurs, schémas de validation.
- Pydantic pour les schémas de validation explicites, indépendants du modèle ORM.
- Asynchrone par défaut : aligné avec les nouvelles contraintes de performances.
- Tests plus simples : code plus modulaire, peu ou pas de surcouche magique.
Alignement avec une architecture hexagonale ou clean : on structure le code métier autour de cas d’usage, pas autour du modèle de persistance.