Cada API break que llegó a producción en mi equipo pasó el CI. Todos. Las unit tests estaban verdes. La integration suite pasó. El deploy salió, y entonces empezaron los mensajes de Slack.

El problema no era que no estuviéramos probando. Estábamos probando lo incorrecto. La mayoría de los pipelines de CI verifican que el código se ejecute. No verifican que el API contract entre producer y consumer siga intacto.

Por qué tus tests existentes no detectan API breaks

Las unit tests ejercitan tu lógica interna. Llaman a tu handler con dependencias mockeadas y hacen assertions sobre la respuesta. Las integration tests levantan todo tu stack y golpean endpoints reales.

Ambos prueban al producer. Verifican que tu API se comporte correctamente cuando se llama de la forma en que actualmente la llamas. No prueban si un consumer externo, construido contra el contract de la semana pasada, todavía puede comunicarse contigo.

Los breaks sutiles son los peligrosos. Renombras un campo de user_id a userId porque tu linter se quejó. Cambias una respuesta 200 para devolver un objeto anidado en lugar de uno plano. Haces que un query parameter requerido que antes era opcional. Tus propios tests se actualizan en el mismo PR, así que todo pasa. Pero cada cliente en producción se rompe.

Esta es la diferencia entre probar código y probar contracts.

Qué cuenta como un breaking API change

Un breaking change es cualquier modificación que haga fallar a un cliente correctamente implementado. No se trata de bugs. Se trata de la promesa que hizo tu API.

Los breaks más comunes caen en tres categorías:

  • Cambios estructurales: Eliminar o renombrar campos, cambiar tipos, alterar anidamiento
  • Cambios de comportamiento: Hacer que parámetros opcionales sean requeridos, cambiar valores por defecto de paginación, modificar la forma de las respuestas de error
  • Cambios de ciclo de vida: Eliminar endpoints, cambiar rutas de URL, deprecar versiones sin aviso

Algunos de estos son obvios. Otros solo lo son si miras la API desde la perspectiva del consumer. La mayoría de los equipos no lo hace.

Capa 1: Detectar breaks estructurales con OpenAPI diffing

Las specs de OpenAPI describen la forma de tu API. Si tratas tu spec como un contract, puedes hacerle diff a la versión anterior y marcar breaking changes antes del merge.

Herramientas como oasdiff comparan dos documentos de OpenAPI y categorizan los cambios como breaking, peligrosos o seguros. Un breaking change es algo como eliminar un campo de respuesta o cambiar un parámetro de opcional a requerido.

Así se ve en un workflow de GitHub Actions:

# .github/workflows/api-contract.yml
name: API Contract Check

on:
  pull_request:
    paths:
      - 'openapi.yaml'

jobs:
  diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Download oasdiff
        run: |
          curl -L https://github.com/Tufin/oasdiff/releases/latest/download/oasdiff_linux_amd64.tar.gz | tar xz
          sudo mv oasdiff /usr/local/bin/

      - name: Check for breaking changes
        run: |
          oasdiff breaking \
            --base origin/main:openapi.yaml \
            --revision openapi.yaml \
            --fail-on WARN

Esto falla el build si el PR introduce un breaking change estructural. Es rápido, deterministic, y detecta los renames y removals que las integration tests se pierden.

Hay un problema. El OpenAPI diffing solo ve el schema. No puede decir si cambiaste el significado de un campo manteniendo el tipo igual. Un campo booleano active que ahora significa “email verified” en lugar de “account enabled” no aparecerá en el diff. El tipo no cambió. El contract sí.

Capa 2: Verificar consumer contracts con Pact

El consumer-driven contract testing invierte el modelo. En lugar de que el API provider afirme su propia corrección, los consumers definen lo que necesitan. Esas expectativas se convierten en contracts que el provider debe satisfacer en el CI.

Así funciona. Tu equipo de frontend escribe un test que dice: “Cuando llamo a GET /users/123, espero un 200 con un body que contenga user_id como string.” Pact registra esta interacción y almacena el contract.

En el lado del provider, tu CI de backend descarga todos los consumer contracts y los reproduce contra el código actual. Si un PR elimina user_id o lo cambia a un número, la provider verification falla. Incluso si tus propios tests pasan.

Un consumer test mínimo con Pact JS:

// consumer.spec.js
const { PactV3 } = require('@pact-foundation/pact');
const { expect } = require('chai');

const provider = new PactV3({
  consumer: 'web-app',
  provider: 'user-service',
  dir: './pacts',
});

describe('GET /users/:id', () => {
  it('returns the user', async () => {
    await provider
      .given('user exists')
      .uponReceiving('a request for user 123')
      .withRequest({
        method: 'GET',
        path: '/users/123',
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          user_id: '123',
          email: 'alice@example.com',
        },
      });

    await provider.executeTest(async (mockserver) => {
      const user = await fetchUser(mockserver.url, '123');
      expect(user.user_id).to.equal('123');
    });
  });
});

La provider verification en CI:

# .github/workflows/verify-contracts.yml
name: Verify Consumer Contracts

on: [pull_request]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Verify pacts
        run: |
          docker run --rm \
            -v $(pwd)/pacts:/pacts \
            pactfoundation/pact-cli \
            verify-provider \
            --provider-app-version ${{ github.sha }} \
            --pact-broker-base-url https://your-pact-broker.io \
            --provider user-service

Esto detecta semantic breaks que el schema diffing no puede ver. Si un consumer depende de un formato de valor específico o un mensaje de error, Pact lo marcará.

Los trade-offs que la mayoría de los equipos ignoran

Ningún enfoque es gratis.

El OpenAPI diffing requiere que mantengas una spec precisa. Si tu spec se genera a partir de code annotations, se mantiene actualizada automáticamente. Si se escribe a mano, se desviará, y el diff se volverá sin sentido. Las specs generadas son mejores para este workflow.

Pact requiere disciplina organizacional. Los consumers deben escribir contract tests. Necesitas un broker para almacenar y versionar contracts. Cuando la expectativa de un consumer está equivocada, alguien tiene que negociar el cambio. Esto crea fricción. Ese es el punto. La fricción previene breaks silenciosos.

Ejecutar ambos es ideal pero no siempre práctico. Si tienes uno o dos consumers críticos, Pact se paga solo rápidamente. Si tienes docenas de consumers anónimos de la API, el OpenAPI diffing es el mejor punto de partida.

Una cosa más: ninguna herramienta detecta regressiones de rendimiento o cambios de autenticación. Un endpoint nuevo que requiere OAuth donde antes no se necesitaba es un breaking change. Tu diff tool podría no marcarlo si el auth schema ya estaba definido en otro lugar. Mantén los ojos abiertos para los huecos.

Preguntas frecuentes

¿Qué es un breaking API change?

Un breaking API change es cualquier modificación a una API que hace fallar a los clientes existentes y correctamente implementados. Esto incluye eliminar campos, cambiar tipos, hacer que parámetros opcionales sean requeridos, o alterar códigos de estado de respuesta para endpoints existentes.

¿En qué se diferencia el API contract testing del integration testing?

Las integration tests verifican que tu sistema funcione como un todo. Las contract tests verifican que la interfaz pública de la API coincida con lo que los consumers esperan. Las integration tests pueden pasar incluso cuando el contract se rompe, si ambos lados del sistema se actualizan en el mismo commit.

¿Puedo usar OpenAPI diffing sin escribir specs a mano?

Sí. Herramientas como oasdiff funcionan con cualquier documento de OpenAPI, incluyendo aquellos generados a partir de code annotations usando bibliotecas como SpringDoc, FastAPI o drf-spectacular. Las specs generadas suelen ser más confiables porque no pueden desviarse de la implementación.

¿Necesito un Pact broker para usar consumer-driven contracts?

Para equipos con más de un par de servicios, sí. El broker almacena contracts, rastrea versiones y muestra qué consumers se ven afectados por un cambio propuesto. Sin él, estás pasando archivos de contract manualmente, lo cual se desmorona rápidamente.

¿Qué hay de GraphQL?

GraphQL tiene semántica diferente de breaking changes. Eliminar un campo es breaking, pero agregar uno es seguro. Herramientas como GraphQL Inspector proporcionan schema diffing similar a las herramientas de OpenAPI. Pact también soporta interacciones de GraphQL.

Empieza con el schema diff

Empieza con OpenAPI diffing. Es el menor esfuerzo y detecta los breaks más comunes. Agrégalo a tu CI hoy, incluso si tu spec es imperfecta. Un chequeo imperfecto es mejor que ningún chequeo.

Una vez que hayas detectado un break en review que habría arruinado el fin de semana de alguien, entenderás por qué esto importa.