Apa Itu Sebenarnya?
API Contract adalah perjanjian formal yang mendefinisikan aturan interaksi antara dua pihak dalam sistem perangkat lunak—biasanya antara consumer (klien) dan provider (server). Perjanjian ini mencakup struktur permintaan yang boleh dikirim, struktur respons yang harus dikembalikan, aturan validasi data, serta kode status yang mungkin muncul. Dalam dunia pengembangan perangkat lunak modern, terutama arsitektur microservices, API Contract menjadi fondasi yang menjamin konsistensi komunikasi lintas tim dan lintas layanan.
Mengapa API Contract begitu penting? Bayangkan sebuah tim frontend membangun antarmuka berdasarkan asumsi bahwa API akan mengembalikan field userEmail, sementara tim backend justru mengirim field email. Tanpa kontrak yang disepakati, kedua tim bekerja berdasarkan tebakan—dan tebakan dalam sistem produksi adalah bencana. API Contract menghilangkan ambiguitas ini dengan menjadi single source of truth. Ketika kontrak sudah didefinisikan, setiap perubahan dapat dideteksi lebih awal melalui pengujian otomatis sebelum kode sampai ke production. Ini menghemat jam-jam debugging dan mencegah breaking change yang berdampak pada pengguna akhir.
Untuk analogi, bayangkan Anda bepergian ke sebuah hotel. Saat reservasi, Anda dan hotel menandatangani kontrak: Anda akan membayar sejumlah tertentu, dan hotel akan menyediakan kamar dengan spesifikasi tertentu—AC, king-size bed, breakfast included. Jika Anda datang dan kamar tidak punya AC, itu pelanggaran kontrak. Anda punya hak untuk komplain karena ada perjanjian tertulis. API Contract bekerja dengan cara yang sama: klien "reservasi" dengan mengirim permintaan sesuai spesifikasi, dan server wajib memenuhi respons sesuai spesifikasi. Jika salah satu pihak melanggar, pengujian kontrak akan gagal dan otomatis menandai ada masalah.
Sebagai contoh konkret, perhatikan API e-commerce sederhana. Kontraknya menyatakan bahwa endpoint GET /products/{id} harus menerima path parameter id berupa integer, dan mengembalikan JSON dengan field id (integer), name (string), dan price (float). Jika seorang developer backend secara tidak sengaja mengubah tipe price dari float menjadi string (misalnya "Rp 50.000"), consumer yang sudah mengandalkan tipe float akan crash. Dengan API Contract, perubahan ini tertangkap sebelum merusak sistem.
Alur Kerjanya
Konsep inti dari API Contract adalah consumer-driven agreement: klien mendeklarasikan apa yang ia butuhkan dari server, dan server berkomitmen untuk memenuhinya. Pendekatan ini membalik paradigma tradisional di mana server mendefinisikan API secara sepihak dan klien hanya mengikuti. Dalam consumer-driven contract testing, klien menulis kontrak berdasarkan kebutuhannya, lalu kontrak tersebut diverifikasi terhadap implementasi server. Jika server memenuhi seluruh kontrak dari semua consumer, maka deployment aman dilakukan.
Prosesnya berlangsung dalam beberapa tahap. Pertama, consumer menulis kontrak—sebuah spesifikasi yang mendeklarasikan permintaan yang akan dikirim dan respons yang diharapkan. Kedua, kontrak tersebut dipublikasikan ke sebuah contract broker—sebuah repositori terpusat yang menyimpan semua kontrak dari berbagai consumer. Ketiga, provider mengambil kontrak-kontrak tersebut dan menjalankan provider verification: ia menguji apakah implementasi aktualnya memenuhi setiap kontrak. Jika semua tes lulus, provider aman di-deploy. Jika ada yang gagal, provider tahu persis consumer mana yang akan terdampak dan field mana yang bermasalah.
Komponen utama dalam ekosistem API Contract ada empat. Contract definition adalah spesifikasi tertulis—bisa berformat JSON, YAML, atau DSL (Domain-Specific Language)—yang memuat daftar interaksi: request path, headers, body, dan expected response. Consumer test adalah pengujian yang dijalankan di sisi klien untuk memastikan bahwa kode klien kompatibel dengan kontrak yang ia tulis; di sini, klien tidak memanggil server sungguhan melainkan menggunakan mock server yang dibangun dari kontrak. Provider verification adalah pengujian di sisi server yang memastikan implementasi real-nya menghasilkan respons sesuai kontrak. Contract broker adalah intermediary—seperti Pact Broker—yang menyimpan kontrak, melacak versi, dan memvisualisasikan matriks kompatibilitas antara consumer dan provider.
Diagram alur sederhananya: Consumer → tulis kontrak → publish ke broker → Provider → ambil kontrak → verifikasi → lulus/gagal. Setiap link dalam rantai ini bisa di-automasi dalam CI/CD pipeline, sehingga setiap pull request atau deployment secara otomatis memvalidasi kompatibilitas.
Penerapan Praktis
Skenario 1: Consumer Test dengan Pact (JavaScript)
Tim frontend membangun dashboard yang memanggil GET /users/1 dan mengharapkan respons berisi id dan name. Mereka menulis consumer test menggunakan Pact JS.
const { Pact } = require('@pact-foundation/pact');
const path = require('path');
const provider = new Pact({
consumer: 'FrontendDashboard',
provider: 'UserService',
port: 1234,
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('User API Consumer', () => {
before(() => provider.setup());
after(() => provider.finalize());
it('should return user by ID', async () => {
await provider.addInteraction({
state: 'user with ID 1 exists',
uponReceiving: 'a request for user with ID 1',
withRequest: {
method: 'GET',
path: '/users/1',
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: { id: 1, name: 'Andi Pratama' },
},
});
const response = await fetch('http://localhost:1234/users/1');
const data = await response.json();
expect(data.id).to.equal(1);
expect(data.name).to.equal('Andi Pratama');
});
});
Setelah tes dijalankan, Pact menghasilkan file kontrak di folder pacts/:
{
"consumer": { "name": "FrontendDashboard" },
"provider": { "name": "UserService" },
"interactions": [
{
"description": "a request for user with ID 1",
"providerState": "user with ID 1 exists",
"request": { "method": "GET", "path": "/users/1" },
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"body": { "id": 1, "name": "Andi Pratama" }
}
}
]
}
File inilah yang kemudian dipublikasikan ke Pact Broker.
Skenario 2: Provider Verification dengan Pact (Node.js)
Tim backend menjalankan verifikasi untuk memastikan implementasi mereka memenuhi kontrak yang dipublikasikan oleh consumer.
const { Verifier } = require('@pact-foundation/pact');
const opts = {
providerBaseUrl: 'http://localhost:8080',
pactBrokerUrl: 'https://broker.example.com',
provider: 'UserService',
providerVersion: '1.0.0',
stateHandlers: {
'user with ID 1 exists': () => {
// Setup data di database test
return db.insertUser({ id: 1, name: 'Andi Pratama' });
},
},
};
new Verifier(opts)
.verifyProvider()
.then((output) => {
console.log('Pact Verification Complete!');
console.log(output);
})
.catch((err) => {
console.error('Pact Verification FAILED:', err);
process.exit(1);
});
Output ketika verifikasi berhasil:
Verifying a pact between FrontendDashboard and UserService
Given user with ID 1 exists
a request for user with ID 1
returns a response which
has status code 200 (OK)
includes headers
"Content-Type" with value "application/json"
has a matching body (OK)
Verification successful
Output ketika verifikasi gagal—misalnya backend mengembalikan username alih-alih name:
Verifying a pact between FrontendDashboard and UserService
Given user with ID 1 exists
a request for user with ID 1
returns a response which
has a matching body (FAILED)
Expected body={"id":1,"name":"Andi Pratama"}
Received body={"id":1,"username":"Andi Pratama"}
Key "name" was not present in the response body
1 interaction failed
Pesan error ini sangat spesifik: consumer FrontendDashboard mengharapkan field name, tetapi provider mengirim username. Developer backend langsung tahu harus memperbaiki apa.
Skenario 3: OpenAPI Spec Sebagai Kontrak
Tidak semua API Contract harus consumer-driven. Banyak tim menggunakan OpenAPI Specification (OAS) sebagai provider-driven contract. Server mendefinisikan spesifikasinya, dan klien serta tooling mengikuti.
openapi: 3.0.3
info:
title: Product Service
version: 1.0.0
paths:
/products/{id}:
get:
summary: Get product by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'404':
description: Product not found
components:
schemas:
Product:
type: object
required: [id, name, price]
properties:
id:
type: integer
name:
type: string
price:
type: number
format: float
Verifikasi otomatis dengan spectral (linter) dan prism (mock server):
# Lint the OpenAPI spec
npx @stoplight/spectral-cli lint openapi.yaml
# Output:
# No errors or warnings found.
# Jalankan mock server dari spec
npx @stoplight/prism-cli mock openapi.yaml
# Output:
# Prism mock server is running on http://127.0.0.1:4010
Klien dapat mengembangkan kode terhadap mock server ini tanpa menunggu backend selesai—paralelisme yang secara dramatis mempercepat siklus pengembangan.
Skenario 4: Contract Testing di CI/CD dengan GitHub Actions
Mengintegrasikan contract testing ke dalam pipeline CI/CD memastikan setiap perubahan kode otomatis tervalidasi.
name: Contract Testing Pipeline
on: [push, pull_request]
jobs:
consumer-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- run: npm run test:consumer
- name: Publish Pact
run: npx pact-broker publish pacts/ --consumer-app-version=$GITHUB_SHA --broker-base-url=https://broker.example.com
provider-verify:
needs: consumer-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- run: npm run start:dev &
- name: Verify Pacts
run: npx pact-broker verify --provider-base-url=http://localhost:8080 --provider=UserService --broker-base-url=https://broker.example.com
Workflow ini memastikan bahwa consumer test dan