Setiap kerusakan API yang masuk ke produksi di tim saya lolos CI. Semuanya. Unit test berwarna hijau. Integration suite lolos. Deploy keluar, dan kemudian pesan Slack mulai masuk.

Masalahnya bukan kami tidak menguji. Kami menguji hal yang salah. Kebanyakan pipeline CI memverifikasi bahwa kode berjalan. Mereka tidak memverifikasi bahwa contract API antara producer dan consumer masih utuh.

Mengapa test yang ada tidak akan menangkap kerusakan API

Unit test menguji logika internal Anda. Mereka memanggil handler Anda dengan dependency yang di-mock dan membuat asersi pada respons. Integration test menjalankan seluruh stack Anda dan mengenai endpoint nyata.

Keduanya menguji producer. Mereka memverifikasi bahwa API Anda berperilaku benar ketika dipanggil dengan cara yang Anda panggil saat ini. Mereka tidak menguji apakah consumer eksternal, yang dibangun berdasarkan contract minggu lalu, masih bisa berkomunikasi dengan Anda.

Kerusakan yang halus adalah yang berbahaya. Anda mengganti nama field dari user_id menjadi userId karena linter Anda mengeluh. Anda mengubah respons 200 untuk mengembalikan objek bersarang alih-alih yang datar. Anda membuat parameter query wajib yang sebelumnya opsional. Test Anda sendiri diperbarui dalam PR yang sama, jadi semuanya lolos. Tapi setiap client di produksi rusak.

Inilah perbedaan antara menguji kode dan menguji contracts.

Apa yang dianggap sebagai breaking change API

Breaking change adalah modifikasi apa pun yang menyebabkan client yang diimplementasikan dengan benar gagal. Ini bukan tentang bug. Ini tentang janji yang dibuat API Anda.

Breaking yang paling umum masuk ke dalam tiga kategori:

  • Perubahan struktural: Menghapus atau mengganti nama field, mengubah tipe, mengubah nesting
  • Perubahan perilaku: Membuat parameter opsional menjadi wajib, mengubah default pagination, memodifikasi bentuk respons error
  • Perubahan siklus hidup: Menghapus endpoint, mengubah path URL, mendeprekasi versi tanpa peringatan

Beberapa dari ini sudah jelas. Yang lain hanya jelas jika Anda melihat API dari perspektif consumer. Kebanyakan tim tidak melakukannya.

Lapisan 1: Tangkap kerusakan struktural dengan OpenAPI diffing

Spesifikasi OpenAPI menggambarkan bentuk API Anda. Jika Anda memperlakukan spec Anda sebagai contract, Anda bisa melakukan diff terhadap versi sebelumnya dan menandai breaking changes sebelum merge.

Tool seperti oasdiff membandingkan dua dokumen OpenAPI dan mengategorikan perubahan sebagai breaking, berbahaya, atau aman. Breaking change adalah sesuatu seperti menghapus field respons atau mengubah parameter dari opsional menjadi wajib.

Berikut tampilannya di workflow 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

Ini gagalkan build jika PR memperkenalkan breaking change struktural. Ini cepat, deterministic, dan menangkap penggantian nama dan penghapusan yang dilewatkan integration test.

Ada satu hal yang perlu diperhatikan. OpenAPI diffing hanya melihat schema. Ini tidak bisa tahu apakah Anda mengubah arti field sambil mempertahankan tipe yang sama. Field boolean active yang sekarang berarti “email terverifikasi” alih-alih “akun aktif” tidak akan muncul di diff. Tipenya tidak berubah. Contract-nya berubah.

Lapisan 2: Verifikasi consumer contracts dengan Pact

Consumer-driven contract testing membalik model. Alih-alih API provider membuat asersi kebenarannya sendiri, consumer mendefinisikan apa yang mereka butuhkan. Ekspektasi tersebut menjadi contracts yang harus dipenuhi provider di CI.

Berikut cara kerjanya. Tim frontend Anda menulis test yang mengatakan: “Ketika saya memanggil GET /users/123, saya mengharapkan 200 dengan body yang berisi user_id sebagai string.” Pact merekam interaksi ini dan menyimpan contract.

Di sisi provider, CI backend Anda menarik semua consumer contracts dan memutar ulang terhadap kode saat ini. Jika PR menghapus user_id atau mengubahnya menjadi number, provider verification gagal. Bahkan jika test Anda sendiri lolos.

Test consumer minimal dengan 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');
    });
  });
});

Provider verification di 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

Ini menangkap semantic breaks yang schema diffing tidak bisa lihat. Jika consumer mengandalkan format nilai atau pesan error tertentu, Pact akan menandainya.

Trade-off yang kebanyakan tim abaikan

Tidak ada pendekatan yang gratis.

OpenAPI diffing mengharuskan Anda memelihara spec yang akurat. Jika spec Anda dihasilkan dari anotasi kode, spec tetap terbarui secara otomatis. Jika ditulis tangan, spec akan menyimpang, dan diff menjadi tidak bermakna. Generated specs lebih baik untuk workflow ini.

Pact memerlukan disiplin organisasi. Consumer harus menulis contract test. Anda memerlukan broker untuk menyimpan dan memverisikan contracts. Ketika ekspektasi consumer salah, seseorang harus menegosiasikan perubahan. Ini menciptakan friksi. Itulah tujuannya. Frikasi mencegah kerusakan diam-diam.

Menjalankan keduanya adalah ideal tapi tidak selalu praktis. Jika Anda memiliki satu atau dua consumer kritis, Pact membayar dirinya sendiri dengan cepat. Jika Anda memiliki puluhan consumer API anonim, OpenAPI diffing adalah titik awal yang lebih baik.

Satu hal lagi: tidak ada tool yang menangkap performance regression atau perubahan autentikasi. Endpoint baru yang memerlukan OAuth di mana sebelumnya tidak diperlukan adalah breaking change. Tool diff Anda mungkin tidak menandainya jika schema auth sudah didefinisikan di tempat lain. Waspadai celah-celah tersebut.

FAQ

Apa itu breaking API change?

Breaking API change adalah modifikasi apa pun pada API yang menyebabkan client yang sudah ada dan diimplementasikan dengan benar gagal. Ini termasuk menghapus field, mengubah tipe, membuat parameter opsional menjadi wajib, atau mengubah kode status respons untuk endpoint yang ada.

Apa bedanya API contract testing dengan integration testing?

Integration test memverifikasi bahwa sistem Anda berfungsi secara utuh. Contract test memverifikasi bahwa antarmuka publik API cocok dengan yang diharapkan consumer. Integration test bisa lolos bahkan ketika contract rusak, jika kedua sisi sistem diperbarui dalam commit yang sama.

Bisakah saya menggunakan OpenAPI diffing tanpa menulis spec tangan?

Ya. Tool seperti oasdiff bekerja dengan dokumen OpenAPI apa pun, termasuk yang dihasilkan dari anotasi kode menggunakan library seperti SpringDoc, FastAPI, atau drf-spectacular. Generated specs seringkali lebih andal karena tidak bisa menyimpang dari implementasi.

Apakah saya memerlukan Pact broker untuk menggunakan consumer-driven contracts?

Untuk tim dengan lebih dari beberapa service, ya. Broker menyimpan contracts, melacak versi, dan menunjukkan consumer mana yang terpengaruh oleh perubahan yang diusulkan. Tanpanya, Anda memindahkan file contract secara manual, yang cepat terpecah.

Bagaimana dengan GraphQL?

GraphQL memiliki semantik breaking change yang berbeda. Menghapus field adalah breaking, tapi menambah field aman. Tool seperti GraphQL Inspector menyediakan schema diffing mirip dengan tool OpenAPI. Pact juga mendukung interaksi GraphQL.

Mulai dengan schema diff

Mulailah dengan OpenAPI diffing. Ini memerlukan usaha paling rendah dan menangkap breaking yang paling umum. Tambahkan ke CI Anda hari ini, meskipun spec Anda belum sempurna. Pemeriksaan yang tidak sempurna lebih baik daripada tidak ada pemeriksaan.

Setelah Anda menangkap suatu breaking di review yang akan merusak akhir pekan seseorang, Anda akan mengerti mengapa ini penting.