Langues disponibles:

Contract First ou mourir en essayant : La seule façon sensée de concevoir des APIs

Ce post a été initialement écrit en anglais. La traduction peut ne pas refléter 100% des idées originales de l'auteur.

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 :

  1. « 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))
  2. « Regarde juste le code. » (Traduction : reverse-engineer notre spaghetti et bonne chance)
  3. « On la documentera plus tard. » (Traduction : on ne la documentera jamais)

J’arrête !

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 .proto est 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.

Au revoir !