En las arquitecturas distribuidas de microservicios, las aplicaciones frontend (React / Next.js) consumen datos de decenas de servicios backend independientes mediante interfaces REST, GraphQL o gRPC. Las estrategias convencionales de validación de integración presentan limitaciones críticas:
La solución arquitectónica radica en la metodología Consumer-Driven Contracts (CDC) y la especificación Pact. En este enfoque, el consumidor (frontend) formula sus requisitos precisos sobre las respuestas de la API, los compila en un documento inmutable en JSON (Pact File) y lo publica en un registro central (Pact Broker). El proveedor del backend debe verificar el contrato en su entorno antes de que ambas partes obtengan autorización para el despliegue.
Los Consumer-Driven Contracts desacoplan temporalmente las pruebas de frontend y backend, garantizando la compatibilidad en tiempo de ejecución.
+-------------------------------------------------------------------------+
| FASE 1: Consumidor Frontend (React / Vitest) |
| |
| 1. La prueba ejecuta una llamada HTTP real contra Pact Mock Server |
| 2. Mock Server valida la petición y devuelve la respuesta simulada |
| 3. Generación del artefacto: order-web-order-service.json |
+-------------------------------------------------------------------------+
|
v pact-broker publish
+-------------------------------------------------------------------------+
| Pact Broker |
| - Almacena el contrato indexado por el Git SHA del frontend |
| - Registra la matriz de compatibilidad: Consumer SHA <-> Provider SHA |
| - Dispara un Webhook al repositorio backend cuando el contrato cambia |
+-------------------------------------------------------------------------+
|
v Trigger CI Provider Verification
+-------------------------------------------------------------------------+
| FASE 2: Proveedor Backend (Node.js / Go) |
| |
| 1. Descarga el contrato desde Pact Broker |
| 2. Configura el Provider State (ej. "el pedido ord-9910 existe en BD")|
| 3. Reproduce la petición del contrato contra controladores reales |
| 4. Valida la estructura de respuesta usando Matchers flexibles |
| 5. Publica los resultados de verificación en Pact Broker |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| FASE 3: Quality Gate de despliegue |
| |
| $ pact-broker can-i-deploy --pacticipant OrderWeb |
| --version $GIT_SHA |
| --to-environment production |
| | |
| +------------------+------------------+ |
| | | |
| [PASS] [FAIL] |
| v v |
| Despliegue Aprobado Despliegue Bloqueado |
| (Backend compatible en producción) (Versión de API incompatible)|
+-------------------------------------------------------------------------+OrderApiClient.ts#Cliente HTTP tipado para consultar los datos de un pedido:
export interface OrderItem {
id: string;
sku: string;
quantity: number;
unitPrice: number;
}
export interface OrderSummary {
orderId: string;
status: 'PENDING' | 'CONFIRMED' | 'SHIPPED';
totalAmount: number;
items: OrderItem[];
createdAt: string;
}
export class OrderApiClient {
constructor(private readonly baseUrl: string) {}
async getOrder(orderId: string): Promise<OrderSummary> {
const response = await fetch(`${this.baseUrl}/api/v2/orders/${orderId}`, {
method: 'GET',
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Order not found: ${orderId}`);
}
throw new Error(`Failed to fetch order: ${response.statusText}`);
}
const data = (await response.json()) as OrderSummary;
return data;
}
}order-consumer.pact.spec.ts#Utilizando @pact-foundation/pact (versión PactV3) con Vitest. La prueba inicia un servidor mock local que valida la petición y exporta el artefacto JSON del contrato:
import { describe, it, expect } from 'vitest';
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
import { OrderApiClient } from '../OrderApiClient';
const { like, eachLike, regex, integer, decimal } = MatchersV3;
// Inicialización de la definición del contrato entre Consumidor y Proveedor
const provider = new PactV3({
consumer: 'OrderWebClient',
provider: 'OrderBackendService',
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('Contrato de API de pedidos: OrderWebClient -> OrderBackendService', () => {
it('obtiene con éxito el resumen del pedido por ID', async () => {
// 1. Declaración de la interacción esperada
provider
.given('el pedido con identificador ord-9910 existe en el sistema')
.uponReceiving('una petición para obtener el pedido ord-9910')
.withRequest({
method: 'GET',
path: '/api/v2/orders/ord-9910',
headers: {
Accept: 'application/json',
},
})
.willRespondWith({
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
body: {
orderId: like('ord-9910'),
status: regex(/^(PENDING|CONFIRMED|SHIPPED)$/, 'CONFIRMED'),
totalAmount: decimal(1499.5),
createdAt: regex(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/,
'2026-09-10T14:30:00.000Z'
),
items: eachLike(
{
id: like('item-001'),
sku: like('KB-MECH-RGB'),
quantity: integer(2),
unitPrice: decimal(749.75),
},
{ min: 1 }
),
},
});
// 2. Realizar la llamada HTTP real contra el servidor mock de Pact
await provider.executeTest(async (mockServer) => {
const client = new OrderApiClient(mockServer.url);
const order = await client.getOrder('ord-9910');
// 3. Validar la respuesta tipada del cliente
expect(order.orderId).toBe('ord-9910');
expect(order.status).toBe('CONFIRMED');
expect(order.items.length).toBeGreaterThanOrEqual(1);
expect(order.items[0].sku).toBe('KB-MECH-RGB');
});
});
});Tras la ejecución exitosa de las pruebas, el contrato generado pacts/OrderWebClient-OrderBackendService.json se sube al Pact Broker central:
# Variables de entorno en CI
export PACT_BROKER_BASE_URL="https://broker.internal.company.net"
export PACT_BROKER_TOKEN="secret-broker-auth-token"
export GIT_COMMIT=$(git rev-parse HEAD)
export GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Publicar contrato especificando el hash de commit y la rama
npx pact-broker publish ./pacts \
--broker-base-url "$PACT_BROKER_BASE_URL" \
--broker-token "$PACT_BROKER_TOKEN" \
--consumer-app-version "$GIT_COMMIT" \
--branch "$GIT_BRANCH"provider.pact.spec.ts#El proveedor descarga los contratos vigentes del broker, arranca un servidor de pruebas y utiliza los controladores de Provider State para alimentar la base de datos antes de reproducir las peticiones:
import { Verifier } from '@pact-foundation/pact';
import { describe, it, beforeAll, afterAll } from 'vitest';
import { app } from '../src/server';
import type { Server } from 'http';
describe('Pact Provider Verification: OrderBackendService', () => {
let server: Server;
const PORT = 8089;
beforeAll(async () => {
// Iniciar servidor en puerto de pruebas dedicado
server = app.listen(PORT);
});
afterAll(async () => {
server.close();
});
it('satisface los contratos de todos los consumidores activos en Pact Broker', async () => {
const verifier = new Verifier({
provider: 'OrderBackendService',
providerBaseUrl: `http://127.0.0.1:${PORT}`,
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
// Publicar resultados de verificación en el broker
publishVerificationResult: !!process.env.CI,
providerVersion: process.env.GIT_COMMIT || '1.0.0',
providerVersionBranch: process.env.GIT_BRANCH || 'main',
// Controladores de estados Provider State
stateHandlers: {
'el pedido con identificador ord-9910 existe en el sistema': async () => {
// Poblar la base de datos con los datos requeridos
await mockDatabase.seedOrder({
id: 'ord-9910',
status: 'CONFIRMED',
totalAmount: 1499.5,
items: [
{ id: 'item-001', sku: 'KB-MECH-RGB', quantity: 2, unitPrice: 749.75 },
],
createdAt: new Date().toISOString(),
});
},
},
});
// Ejecutar la suite de verificación
await verifier.verifyProvider();
});
});can-i-deploy en GitHub Actions#Antes de proceder al despliegue, el pipeline de CI consulta la matriz de compatibilidad. Si el entorno de destino no cuenta con una versión del backend verificada, el pipeline detiene el despliegue de inmediato:
name: Deploy Consumer Frontend
on:
push:
branches: [main]
jobs:
contract-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# Generación del contrato con Vitest
- run: pnpm test:pact
# Publicación del contrato en Pact Broker
- name: Publish Pact Contract
run: |
npx pact-broker publish ./pacts \
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--broker-token ${{ secrets.PACT_BROKER_TOKEN }} \
--consumer-app-version ${{ github.sha }} \
--branch main
quality-gate:
needs: contract-test
runs-on: ubuntu-latest
steps:
# Consultar matriz: ¿es seguro desplegar este commit en producción?
- name: Can I Deploy?
run: |
npx pact-broker can-i-deploy \
--pacticipant OrderWebClient \
--version ${{ github.sha }} \
--to-environment production \
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--broker-token ${{ secrets.PACT_BROKER_TOKEN }}
- name: Deploy to Cloud
run: pnpm run deploy:prodUtilizar constantes fijas convierte la prueba de contrato en una aserción frágil de base de datos. Si el backend responde con un número o marca temporal válida pero distinta, la verificación fallará sin que exista un fallo de integración real.
// ❌ INCORRECTO: Literales estáticos rígidos
body: {
totalAmount: 1499.50, // Fallará si en la base de datos el pedido totaliza 2000.00
createdAt: "2026-09-10T12:00:00Z", // Fallará ante marcas de tiempo dinámicas
}
// ✅ CORRECTO: Matchers de esquema flexibles
body: {
totalAmount: MatchersV3.decimal(1499.50), // Valida tipo numérico decimal
createdAt: MatchersV3.iso8601DateTime(), // Valida formato ISO-8601
}Las pruebas de contrato validan el protocolo de comunicación y la forma de los datos (cabeceras, códigos HTTP, tipos del payload). Intentar codificar en Pact todas las combinaciones de descuentos o reglas fiscales sobrecarga el contrato con decenas de escenarios redundantes. Regla de oro: 1 contrato para 200 OK + 1 para 404 Not Found + 1 para 400 Bad Request. Las reglas de cálculo deben validarse en las pruebas unitarias del backend.
Lanzar cambios sin consultar la matriz de compatibilidad invalida la seguridad del sistema: si el backend renombró un campo en una rama sin desplegarlo en producción, el frontend fallará inmediatamente ante los usuarios con TypeError: Cannot read properties of undefined.
Solución: Exigir siempre pact-broker can-i-deploy como paso bloqueante en CI.
El frontend declara rutas, códigos HTTP y esquemas de datos usando MatchersV3 de @pact-foundation/pact.
Ejecutar la suite en Vitest para validar la lógica del cliente y generar el archivo de contrato en pacts.
Subir el artefacto al broker etiquetando el Git SHA y la rama mediante pact-broker publish.
Lanzar el verificador en el backend ejecutando stateHandlers para preparar el estado requerido en la BD.
Consultar la matriz de compatibilidad en el pipeline de CI/CD antes de permitir el pase a producción.