Contract First ou mourir en essayant : La seule façon sensée de concevoir des APIs
Je dois vider mon sac. Quelque chose qui ronge mon âme depuis la première fois que j’ai rejoint un projet en cours de développement et que j’ai posé la question fatidique : « Où est la documentation de l’API ? »
La réponse était invariablement l’une des suivantes :
- « Regarde la collection Postman. » (Traduction : un cimetière de 200 requêtes, dont la moitié est obsolète, nommées des choses comme
GET users FINAL v2 (copie)) - « Regarde juste le code. » (Traduction : reverse-engineer notre spaghetti et bonne chance)
- « On la documentera plus tard. » (Traduction : on ne la documentera jamais)

Ceci, mes amis, est la conséquence du design d’API Code First. Et je suis ici pour vous dire que c’est une maladie. Une maladie contagieuse, qui tue les projets et se propage dans les équipes comme un virus dans un bureau mal ventilé.
La scène du crime : Code First
Laissez-moi vous peindre un tableau. Vous êtes un développeur backend. Vous recevez un ticket : « Créer un endpoint pour récupérer les transactions utilisateur. » Vous ouvrez votre IDE, vous créez un Controller, vous connectez un Service, vous retournez un DTO que vous avez inventé sur le champ, et vous poussez sur develop. Terminé. Livrez-le.
Pendant ce temps, le développeur frontend attend. Il vous demande : « À quoi ressemble la réponse ? » Vous dites : « Appelle-le et tu verras. » Il l’appelle. Il voit un champ appelé txn_dt. Il demande : « C’est un timestamp ? Une chaîne ? Quel fuseau horaire ? » Vous haussez les épaules. « C’est ce que LocalDateTime de Java sérialise par défaut. »
Félicitations. Vous venez de créer un désastre de communication enveloppé dans un sandwich de dette technique.
Le développeur mobile, qui a rejoint l’appel cinq minutes en retard, doit maintenant implémenter le même endpoint. Il regarde la « documentation » (la collection Postman, vous vous souvenez ?) et trouve une requête datant de trois sprints qui renvoie une structure complètement différente parce que quelqu’un a refactoré le DTO et a oublié de tout mettre à jour.
C’est le Code First. Vous écrivez le code, et le contrat est ce que le code produit par hasard. La spécification de l’API est une réflexion après coup, un effet secondaire, un accident.
Le salut : Contract First
Imaginez maintenant un univers parallèle où la raison prévaut.
Avant que quiconque n’écrive une seule ligne de Java, Go ou quel que soit le poison que vous préférez, l’équipe s’assoit et définit le contrat. Ce contrat est un fichier .yaml ou .json écrit en OpenAPI (anciennement Swagger). Il spécifie :
- Chaque endpoint.
- Chaque méthode HTTP.
- Chaque paramètre de requête et son type.
- Chaque corps de réponse et sa structure.
- Chaque code d’erreur possible et ce qu’il signifie.
Ce fichier devient la source unique de vérité. Le backend génère des squelettes de serveur à partir de celui-ci. Le frontend génère des SDK clients à partir de celui-ci. L’équipe mobile génère ses modèles à partir de celui-ci. Tout le monde travaille contre la même spécification, synchronisé comme un orchestre bien dirigé.
openapi: 3.0.3
info:
title: Transaction API
version: 1.0.0
paths:
/users/{userId}/transactions:
get:
summary: Fetch user transactions
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: A list of transactions
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Transaction'
'404':
description: User not found
components:
schemas:
Transaction:
type: object
properties:
id:
type: string
format: uuid
amount:
type: integer
description: Amount in cents
currency:
type: string
example: "EUR"
timestamp:
type: string
format: date-time
required:
- id
- amount
- currency
- timestamp
Regardez ça. Magnifique. Explicite. Aucune ambiguïté. Le timestamp est un date-time. Le amount est en centimes (pas de cauchemars à virgule flottante). Le userId est un UUID, pas « n’importe quelle chaîne que vous avez envie d’envoyer ».
Si le backend s’écarte de ce contrat, le code généré ne compilera pas. Si le frontend s’attend à un champ différent, le client généré lui criera dessus. Le contrat impose la discipline.
« Mais écrire du YAML, c’est ennuyeux »
Oui. Ça l’est. Vous savez ce qui est aussi ennuyeux ? Passer quatre heures sur un appel vidéo à déboguer pourquoi l’application Android plante parce que quelqu’un dans le backend a décidé de renommer user_id en userId sans prévenir personne.
Vous savez ce qui est ennuyeux ? Réécrire des tests d’intégration parce que la structure de la réponse a changé et que personne n’a mis à jour les mocks.
Vous savez ce qui est écœurant d’ennui ? Être le pauvre type qui rejoint un projet de deux ans et doit comprendre 150 endpoints en lisant des fichiers Controller dispersés dans 47 packages sans aucune documentation.
Écrire le contrat au préalable est un investissement. C’est comme se brosser les dents : fastidieux sur le moment, mais cela évite un traitement de canal plus tard.
L’outillage : Édition Java
Puisque j’ai promis des exemples de code, laissez-moi vous montrer comment cela fonctionne dans l’écosystème Spring. Nous utilisons un plugin Maven appelé openapi-generator-maven-plugin. Vous le pointez vers votre contrat, et il génère les interfaces pour vous.
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.2.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api/openapi.yaml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>me.doismiu.api.generated</apiPackage>
<modelPackage>me.doismiu.api.generated.model</modelPackage>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useTags>true</useTags>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
Exécutez mvn compile, et soudain vous avez une interface Java qui ressemble à ceci :
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen")
@Tag(name = "Transactions")
public interface TransactionsApi {
@Operation(summary = "Fetch user transactions")
@GetMapping(value = "/users/{userId}/transactions", produces = "application/json")
ResponseEntity<List<Transaction>> getUserTransactions(
@PathVariable("userId") UUID userId
);
}
Votre Controller implémente simplement cette interface :
@RestController
public class TransactionsController implements TransactionsApi {
private final TransactionService service;
public TransactionsController(TransactionService service) {
this.service = service;
}
@Override
public ResponseEntity<List<Transaction>> getUserTransactions(UUID userId) {
return ResponseEntity.ok(service.findByUser(userId));
}
}
Si vous essayez de changer le type de retour pour quelque chose qui ne correspond pas au contrat, le compilateur vous giflera. Si vous essayez d’ajouter un paramètre qui n’est pas dans la spec, le compilateur vous giflera encore plus fort. Le contrat est la loi, et le code doit obéir.
Le problème culturel
Voici la vérité inconfortable : Contract First n’est pas un défi technique. C’est un problème culturel.
Les développeurs y résistent parce que cela ressemble à du « travail supplémentaire ». Les product managers y résistent parce que cela « ralentit la livraison ». Tout le monde veut aller vite et casser des choses, sauf que les choses qu’ils cassent sont la santé mentale de leurs collègues et la stabilité du système.
Contract First vous oblige à réfléchir avant de coder. Il vous oblige à avoir des conversations sur les types de données, les cas limites et la gestion des erreurs avant qu’ils ne deviennent des incidents de production. Il impose de la discipline dans une industrie qui vénère le « move fast ».
Et c’est exactement pourquoi la plupart des équipes refusent de l’adopter. La discipline est difficile. Le chaos est facile. Jusqu’à ce qu’il ne le soit plus.
Quand NE PAS utiliser Contract First
Je serai juste. Il y a des scénarios où le Code First a du sens :
- Prototypage : Vous explorez, vous ne savez pas encore à quoi ressemblera l’API, et vous devez itérer rapidement. D’accord. Mais au moment où cela passe en production, vous écrivez ce contrat.
- Microservices internes avec gRPC : Si vous utilisez déjà Protobuf (comme je l’ai critiqué précédemment), le fichier
.protoest votre contrat. Vous faites déjà du Contract First sans l’appeler ainsi. - Projets solo : Si vous êtes le seul développeur, frontend et backend, vous pouvez garder le contrat dans votre tête. Mais au moment où quelqu’un d’autre rejoint, écrivez-le.
Pour tout le reste, surtout les équipes de plus d’une personne, surtout les projets censés vivre plus longtemps qu’un hackathon, Contract First est non négociable.
Conclusion
J’ai vu des projets mourir parce que personne ne savait ce que l’API était censée faire. J’ai vu des développeurs démissionner parce qu’ils passaient plus de temps à reverse-engineer des endpoints non documentés qu’à construire des fonctionnalités. J’ai vu des pannes de production causées par des « changements mineurs » qui ont cassé tous les clients parce qu’il n’y avait pas de contrat pour valider.
Contract First n’est pas une méthodologie. C’est une stratégie de survie. C’est la différence entre construire sur un sol solide et construire sur des sables mouvants les yeux bandés.
Écrivez le contrat. Générez le code. Dormez la nuit.
Ou ne le faites pas. Continuez à vivre dans le chaos. Continuez à déboguer des collections Postman à 2 heures du matin. Continuez à expliquer à l’équipe mobile pourquoi le champ s’appelle maintenant transactionDate au lieu de txn_dt pour la troisième fois ce mois-ci.
À vous de voir.
