У розподіленій мікросервісній архітектурі сучасних вебзастосунків фронтенд (React / Next.js) взаємодіє з десятками незалежних бекенд-сервісів через REST, GraphQL або gRPC API. Традиційні підходи до валідації міжсервісної інтеграції мають критичні обмеження:
Архітектурним вирішенням є методологія Consumer-Driven Contracts (CDC) та специфікація Pact. У цій моделі саме споживач (фронтенд) формулює свої точні вимоги до відповідей API, компілює їх у незмінний JSON-документ (Pact File) і публікує в центральний реєстр (Pact Broker). Бекенд-провайдер зобов'язаний верифікувати контракт на своїй стороні перед тим, як обидва сервіси отримають дозвіл на реліз.
Життєвий цикл Consumer-Driven Contracts усуває синхронну залежність тестування фронтенду та бекенду в часі, гарантуючи сумісність у рантаймі.
+-------------------------------------------------------------------------+
| ФАЗА 1: Фронтенд-споживач (React / Vitest) |
| |
| 1. Тест виконує реальний HTTP-запит до локального Pact Mock Server |
| 2. Mock Server звіряє параметри та повертає синтетичну відповідь |
| 3. Генерація артефакту: order-web-order-service.json |
+-------------------------------------------------------------------------+
|
v pact-broker publish
+-------------------------------------------------------------------------+
| Pact Broker |
| - Збереження контракту з прив'язкою до Git SHA фронтенду |
| - Матриця сумісності: Consumer SHA <-> Provider SHA |
| - Відправлення Webhook у репозиторій бекенду при зміні контракту |
+-------------------------------------------------------------------------+
|
v Trigger CI Provider Verification
+-------------------------------------------------------------------------+
| ФАЗА 2: Бекенд-провайдер (Node.js / Go) |
| |
| 1. Завантаження контракту з Pact Broker |
| 2. Встановлення Provider State (наприклад: "order ord-9910 exists") |
| 3. Відтворення запиту з контракту на реальний контролер сервера |
| 4. Перевірка структури відповіді за допомогою Matchers |
| 5. Публікація результатів перевірки назад у Pact Broker |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| ФАЗА 3: Quality Gate перед деплоєм у Production |
| |
| $ pact-broker can-i-deploy --pacticipant OrderWeb |
| --version $GIT_SHA |
| --to-environment production |
| | |
| +------------------+------------------+ |
| | | |
| [PASS] [FAIL] |
| v v |
| Дозвіл на деплой Блокування пайплайну |
| (Бекенд у проді сумісний) (Несумісна версія API) |
+-------------------------------------------------------------------------+OrderApiClient.ts#Типізований HTTP-клієнт для отримання даних замовлення:
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#Використовуємо бібліотеку @pact-foundation/pact (версія PactV3) разом із Vitest. Під час виконання тесту піднімається локальний мок-сервер, який валідує запит і формує JSON-файл контракту:
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;
// Ініціалізація визначення контракту між Споживачем та Постачальником
const provider = new PactV3({
consumer: 'OrderWebClient',
provider: 'OrderBackendService',
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('Контракт API замовлень: OrderWebClient -> OrderBackendService', () => {
it('успішно повертає зведення замовлення за ідентифікатором', async () => {
// 1. Опис очікуваної взаємодії
provider
.given('замовлення з ідентифікатором ord-9910 існує в системі')
.uponReceiving('запит на отримання інформації про замовлення 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. Виконання мережевого виклику до локального мок-сервера Pact
await provider.executeTest(async (mockServer) => {
const client = new OrderApiClient(mockServer.url);
const order = await client.getOrder('ord-9910');
// 3. Перевірка типізованої відповіді клієнта
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');
});
});
});Після успішного виконання тесту скомпільований файл pacts/OrderWebClient-OrderBackendService.json завантажується в централізований брокер:
# Змінні оточення 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)
# Публікація контракту із зазначенням комміту та гілки
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#Провайдер завантажує контракти з брокера, запускає тестовий сервер і задіює обробники Provider State для підготовки бази даних перед кожним запитом:
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 () => {
// Запуск сервера на виділеному порту
server = app.listen(PORT);
});
afterAll(async () => {
server.close();
});
it('задовольняє контрактам усіх активних споживачів із 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,
// Публікація результатів перевірки назад у брокер
publishVerificationResult: !!process.env.CI,
providerVersion: process.env.GIT_COMMIT || '1.0.0',
providerVersionBranch: process.env.GIT_BRANCH || 'main',
// Обробники початкових станів Provider State
stateHandlers: {
'замовлення з ідентифікатором ord-9910 існує в системі': async () => {
// Наповнення бази даних тестовими даними
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(),
});
},
},
});
// Запуск верифікації
await verifier.verifyProvider();
});
});can-i-deploy в GitHub Actions#Перед виконанням деплою в продакшн конвеєр опитує матрицю сумісності версій. Якщо цільове середовище не містить верифікованої версії бекенду, деплой негайно переривається:
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
# Генерація контракту за допомогою Vitest
- run: pnpm test:pact
# Публікація контракту в 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:
# Запит матриці: чи дозволено деплоїти цей комміт у production?
- 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:prodВикористання статичних констант замість зіставників типів перетворює контрактний тест на крихку перевірку бази даних. Якщо бекенд поверне інше допустиме число чи динамічний таймстамр, верифікація впаде без реального порушення контракту.
// ❌ ПОМИЛКА: фіксація точного числа та рядка
body: {
totalAmount: 1499.50, // Впаде, якщо в базі замовлення на 2000.00
createdAt: "2026-09-10T12:00:00Z", // Впаде при генерації поточного часу
}
// ✅ ПРАВИЛЬНО: гнучкі матчери структури
body: {
totalAmount: MatchersV3.decimal(1499.50), // Перевіряє число з плаваючою крапкою
createdAt: MatchersV3.iso8601DateTime(), // Перевіряє формат дати ISO
}Контрактні тести перевіряють форму та протокол комунікації (заголовки, статус-коди, структуру корисного навантаження JSON). Спроба закодувати в Pact усі можливі варіації нарахування знижок чи податків призводить до розростання десятків надлишкових контрактів. Правило: 1 контракт для 200 OK + 1 для 404 Not Found + 1 для 400 Bad Request. Розрахунки бізнес-правил повинні тестуватися юніт-тестами самого бекенду.
Реліз клієнта без опитування матриці сумісності нівелює безпеку: якщо бекенд перейменував поле, але ще не викатив зміни на бойовий сервер, фронтенд впаде у користувачів із помилкою TypeError: Cannot read properties of undefined.
Рішення: обов'язково налаштовувати pact-broker can-i-deploy як блокуючий крок у CI.
Фронтенд описує маршрути, коди відповідей та структури JSON за допомогою MatchersV3 у @pact-foundation/pact.
Запуск тестового набору у Vitest та створення файлу контракту в директорії pacts.
Завантаження артефакту в брокер із зазначенням поточного Git SHA та гілки через pact-broker publish.
Запуск перевірки на бекенді з викликом stateHandlers для підготовки тестових даних у БД.
Запит матриці сумісності в CI/CD конвеєрі перед просуванням релізу в робоче середовище.