Idiomes disponibles:

Contract First o Morir en el Intento: La Única Forma Sensata de Diseñar APIs

Aquesta publicació va ser originalment escrita en anglès. La traducció pot no reflectir el 100% de les idees originals de l'autor.

Necessito descarregar alguna cosa del pit. Alguna cosa que ha estat festerant a la meva ànima des de la primera vegada que em vaig unir a un projecte a mitjan desenvolupament i vaig fer la pregunta fatídica: “On està la documentació de l’API?”

La resposta, invariablement, era una de les següents:

  1. “Mira la col·lecció de Postman.” (Traducció: un cementiri de 200 peticions, la meitat de les quals estan desactualitzades, anomenades coses com GET users FINAL v2 (còpia))
  2. “Simplement mira el codi.” (Traducció: reverse-engineer la nostra espagueti i bona sort)
  3. “La documentarem més tard.” (Traducció: mai la documentarem)

Ho Deixo!

Això, amics meus, és la conseqüència del disseny d’API Code First. I sóc aquí per dir-vos que és una malaltia. Una malaltia contagiosa, que mata projectes i s’estén pels equips com un virus per una oficina mal ventilada.

L’Escena del Crim: Code First

Deixeu-me pintar-vos un quadre. Sou un desenvolupador de backend. Reps un tiquet: “Crea un endpoint per obtenir les transaccions de l’usuari.” Obreu el vostre IDE, creeu un Controlador, connecteu un Servei, retorneu algun DTO que heu inventat al moment, i feu push a develop. Fet. Envieu-ho.

Mentrestant, el desenvolupador de frontend està esperant. Us pregunta: “Com és la resposta?” Dieu: “Simplement crida-ho i veuràs.” Ell ho crida. Veu un camp anomenat txn_dt. Pregunta: “Això és un timestamp? Una cadena? Quina zona horària?” Encongeixes les espatlles. “És el que sigui que LocalDateTime de Java serialitzi per defecte.”

Enhorabona. Acabes de crear un desastre de comunicació embolicat en un entrepà de deute tècnic.

El desenvolupador mòbil, que s’ha unit a la trucada cinc minuts tard, ara ha d’implementar el mateix endpoint. Mira la “documentació” (la col·lecció de Postman, recordes?) i troba una petició de tres sprints enrere que retorna una estructura completament diferent perquè algú va refactoritzar el DTO i es va oblidar d’actualitzar qualsevol cosa.

Això és Code First. Escrius el codi, i el contracte és el que sigui que el codi produeixi per casualitat. L’especificació de l’API és una reflexió posterior, un efecte secundari, un accident.

La Salvació: Contract First

Ara imagineu un univers paral·lel on preval la seny.

Abans que ningú escrigui una sola línia de Java, Go, o qualsevol verí que preferiu, l’equip s’asseu i defineix el contracte. Aquest contracte és un fitxer .yaml o .json escrit en OpenAPI (abans Swagger). Especifica:

  • Cada endpoint.
  • Cada mètode HTTP.
  • Cada paràmetre de petició i el seu tipus.
  • Cada cos de resposta i la seva estructura.
  • Cada codi d’error possible i què significa.

Aquest fitxer es converteix en la font única de veritat. El backend genera esquelets de servidor a partir d’ell. El frontend genera SDKs de client a partir d’ell. L’equip mòbil genera els seus models a partir d’ell. Tothom treballa contra la mateixa especificació, sincronitzat com una orquestra ben dirigida.

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

Mireu això. Preciós. Explícit. Sense ambigüitat. El timestamp és un date-time. L’amount és en cèntims (sense malsons de punt flotant). El userId és un UUID, no “qualsevol cadena que et vingui de gust enviar.”

Si el backend es desvia d’aquest contracte, el codi generat no es compilarà. Si el frontend espera un camp diferent, el client generat els cridarà. El contracte imposa disciplina.

“Però Escriure YAML és Avorrit”

Sí. Ho és. Sabeu què més és avorrit? Passar quatre hores en una trucada de vídeo depurant per què l’aplicació d’Android es bloqueja perquè algú al backend va decidir canviar user_id a userId sense dir-ho a ningú.

Sabeu què és avorrit? Reescrivint proves d’integració perquè l’estructura de resposta va canviar i ningú va actualitzar els mocks.

Sabeu què és ànimament aclaparadorament avorrit? Ser el pobre bastard que s’uneix a un projecte de dos anys i ha d’entendre 150 endpoints llegint fitxers de Controlador escampats per 47 paquets sense cap documentació.

Escriure el contracte per endavant és una inversió. És com rentar-se les dents: tediós en el moment, però evita una endodòncia més tard.

Les Eines: Edició Java

Com que vaig prometre exemples de codi, deixeu-me mostrar-vos com funciona això en l’ecosistema Spring. Utilitzem un plugin de Maven anomenat openapi-generator-maven-plugin. L’apuntes al teu contracte, i genera les interfícies per tu.

<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>

Executeu mvn compile, i de sobte teniu una interfície Java que té aquest aspecte:

@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
    );
}

El vostre Controlador simplement implementa aquesta interfície:

@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 intenteu canviar el tipus de retorn a alguna cosa que no coincideixi amb el contracte, el compilador us donarà una bufetada. Si intenteu afegir un paràmetre que no està a l’especificació, el compilador us donarà una bufetada més forta. El contracte és llei, i el codi ha d’obeir.

El Problema Cultural

Aquí teniu la veritat incòmoda: Contract First no és un repte tècnic. És un repte cultural.

Els desenvolupadors s’hi resisteixen perquè sembla “treball extra.” Els gestors de producte s’hi resisteixen perquè “alenteix la lliurament.” Tothom vol moure’s ràpid i trencar coses, excepte que les coses que trenquen són la seny dels seus col·legues i l’estabilitat del sistema.

Contract First us obliga a pensar abans de codificar. Us obliga a tenir converses sobre tipus de dades, casos límit i maneig d’errors abans que es converteixin en incidents de producció. Imposa disciplina en una indústria que adora “moure’s ràpid.”

I això és exactament per què la majoria d’equips es nega a adoptar-ho. La disciplina és dura. El caos és fàcil. Fins que no ho és.

Quan NO Utilitzar Contract First

Seré just. Hi ha escenaris on Code First té sentit:

  • Prototipatge: Esteu explorant, no sabeu com serà l’API encara, i necessiteu iterar ràpid. D’acord. Però el moment que vagi a producció, escriviu aquest contracte.
  • Microserveis interns amb gRPC: Si ja esteu utilitzant Protobuf (com vaig despotricar prèviament), el fitxer .proto és el vostre contracte. Ja esteu fent Contract First sense dir-ho.
  • Projectes en solitari: Si sou l’únic desenvolupador, frontend i backend, podeu mantenir el contracte al cap. Però el moment que s’uneixi algú altre, escriviu-lo.

Per a tot la resta, especialment equips més grans d’una persona, especialment projectes que s’espera que visquin més temps que un hackathon, Contract First és no negociable.

Conclusió

He vist projectes morir perquè ningú sabia què se suposava que havia de fer l’API. He vist desenvolupadors deixar-ho perquè van passar més temps fent reverse-engineering d’endpoints no documentats que construint funcionalitats. He vist caigudes de producció causades per “canvis menors” que van trencar tots els clients perquè no hi havia cap contracte per validar-se.

Contract First no és una metodologia. És una estratègia de supervivència. És la diferència entre construir sobre terra ferma i construir sobre sorra movedissa amb els ulls embenats.

Escriviu el contracte. Genereu el codi. Dormiu de nit.

O no. Continueu vivint en el caos. Continueu depurant col·leccions de Postman a les 2 AM. Continueu explicant a l’equip mòbil per què el camp ara es diu transactionDate en lloc de txn_dt per tercera vegada aquest mes.

La decisió és vostra.

Adéu!