Bab lalu kita menetapkan bahwa Order Service akan memanggil Inventory Service secara sinkron lewat HTTP. Sebelum satu baris kode RestClient pun ditulis di sisi Order Service (itu baru datang di bab 29), ada satu pertanyaan yang harus dijawab lebih dulu: bentuk API-nya seperti apa persis? Endpoint-nya apa, parameternya apa, respons suksesnya berbentuk apa, respons gagalnya berbentuk apa? Bab ini soal menjawab pertanyaan itu secara sengaja, sebagai dokumen, sebelum kode implementasinya matang — pendekatan yang disebut contract-first.
Contract-First vs Code-First
Ada dua cara umum menentukan bentuk API sebuah service:
Code-first adalah yang paling sering terjadi secara default, bahkan tanpa disadari: kamu langsung menulis controller, dan bentuk API-nya "ya begitu saja, sesuai apa yang kebetulan ditulis di kode". Kontraknya tidak pernah didokumentasikan secara eksplisit — kalau ada pihak lain (tim lain, service lain, atau kamu sendiri enam bulan kemudian) ingin tahu bentuk API-nya, satu-satunya cara adalah membaca kode controller-nya langsung, atau lebih buruk lagi, mencoba-coba lewat Postman sampai ketemu polanya. Setiap kali controller berubah, kontrak "berubah" secara diam-diam ikut berubah, tanpa jejak.
Contract-first membalik urutannya: kamu menulis dulu spesifikasi API-nya sebagai dokumen terpisah — biasanya format OpenAPI (untuk REST) atau Protobuf (untuk gRPC) — sebelum atau sejajar dengan implementasi. Dokumen itu jadi sumber kebenaran yang eksplisit, bisa dibaca tanpa perlu buka kode, dan bisa dijadikan pegangan oleh siapa pun yang akan mengonsumsi API itu (termasuk tim lain yang belum tentu punya akses ke kodenya).
Kenapa ini penting justru di titik ini dalam seri kita? Karena mulai sekarang ada dua tim implisit — sisi Inventory Service dan sisi Order Service (di project nyata, sering kali dua tim sungguhan) — yang harus sepakat soal bentuk API sebelum keduanya selesai dibangun. Kalau kontraknya cuma "ada di kepala satu orang" atau "lihat saja kodenya nanti", kesalahpahaman baru ketahuan pas integrasi, biasanya di waktu yang paling tidak nyaman.
Kontrak Asli Inventory Service
Di project ini, kontrak Inventory Service ditulis lebih dulu di inventory-service/openapi/inventory-service.yaml — sengaja ditulis tangan, sebelum implementasinya benar-benar dipoles. Ini filenya, lengkap:
openapi: 3.0.3
info:
title: Inventory Service API
description: >
Kontrak API Inventory Service - ditulis SEBELUM implementasi (contract-first),
seri Belajar Kotlin Backend untuk Microservice, Fase 4.3.
version: 1.0.0
servers:
- url: http://localhost:8081
description: Local development
paths:
/api/inventory/{itemName}:
get:
summary: Cek stok satu item
operationId: getStock
parameters:
- name: itemName
in: path
required: true
schema:
type: string
example: Keyboard Mechanical
responses:
"200":
description: Item ditemukan, stok dikembalikan
content:
application/json:
schema:
$ref: "#/components/schemas/StockResponse"
"404":
description: Item tidak terdaftar di Inventory Service
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
schemas:
StockResponse:
type: object
required: [itemName, stock]
properties:
itemName:
type: string
example: Keyboard Mechanical
stock:
type: integer
format: int32
example: 12
ErrorResponse:
type: object
required: [error]
properties:
error:
type: string
example: "Item 'Webcam' tidak terdaftar di Inventory Service"
Pendek, tapi lengkap sebagai kontrak. Mari bedah bagian-bagiannya.
Struktur OpenAPI 3.0, Singkat
OpenAPI punya beberapa bagian standar yang selalu muncul dalam bentuk serupa:
info— metadata dasar: judul, deskripsi, versi kontrak.version: 1.0.0di sini penting kalau nanti kontrak berubah secara breaking — versi yang naik jadi sinyal bagi konsumen API.servers— daftar base URL tempat API ini bisa diakses. Di sini cuma ada satu,http://localhost:8081, cocok denganserver.port: 8081yang akan kita lihat lagi di bab 29.paths— inti dari kontrak: daftar endpoint dan method HTTP-nya. Di sini cuma satu:GET /api/inventory/{itemName}.operationId: getStock— nama unik untuk operasi ini. Berguna kalau kontrak ini nanti dipakai untuk men-generate kode client secara otomatis (client generator butuh nama fungsi, danoperationIditulah yang jadi sumbernya).parameters— di siniitemNamedidefinisikan sebagai path parameter, wajib ada (required: true), bertipe string, lengkap dengan contoh nilai (example).responses— bagian paling bernilai dari sebuah kontrak API: setiap status code yang mungkin dikembalikan, didaftar secara eksplisit, bukan cuma jalur sukses. Di sini ada dua:"200"untuk sukses (mengembalikanStockResponse) dan"404"untuk item yang tidak terdaftar (mengembalikanErrorResponse). Siapa pun yang membaca kontrak ini tahu persis dua kemungkinan hasil tanpa perlu membaca kode Kotlin-nya sama sekali.components/schemas— definisi bentuk data yang dipakai berulang diresponseslewat$ref.StockResponsemendeklarasikan dua field wajib (required: [itemName, stock]) dengan tipe masing-masing (stringdaninteger), danErrorResponsemendeklarasikan satu fielderrorbertipe string.
Bandingkan ini dengan kelas Kotlin yang akan kamu lihat sendiri implementasinya nanti di bab 29: data class StockResponse(val itemName: String, val stock: Int). Perhatikan betapa dekatnya — itu bukan kebetulan. Kontrak yang ditulis di awal jadi acuan bentuk data class yang ditulis belakangan, bukan sebaliknya.
Kenapa Tidak Pakai springdoc-openapi Saja?
Kalau kamu pernah dengar springdoc-openapi (yang men-generate dokumentasi Swagger UI otomatis dari anotasi di controller), kamu mungkin bertanya kenapa tidak dipakai di sini saja — bukankah lebih praktis? Jawabannya: springdoc-openapi itu code-first, bukan contract-first — ia membaca kode yang sudah ada dan menghasilkan dokumentasi dari situ, urutannya terbalik dari yang sedang kita praktikkan di bab ini. Untuk fokus belajar seri ini, springdoc-openapi sengaja tidak dipasang, supaya kita tidak menambah dependency ekstra hanya untuk mendemonstrasikan satu prinsip desain.
Tapi jangan salah paham — meski ditulis tangan, file inventory-service.yaml di atas adalah kontrak yang sah dan lengkap dengan sendirinya. Ia valid sebagai dokumen OpenAPI 3.0, bisa dibaca manusia tanpa tooling tambahan apa pun, dan bisa langsung disuap ke tool client-generation (seperti OpenAPI Generator) kapan pun dibutuhkan nanti — bukan sekadar dokumentasi hiasan yang tidak sinkron dengan kenyataan.
Kontrak Ini Sudah Ada — Sekarang Perlu Pintu Masuk
Kontrak di atas menjelaskan bentuk API Inventory Service seandainya dipanggil langsung. Tapi begitu sistem punya lebih dari satu service, muncul pertanyaan praktis berikutnya: apakah client (atau Order Service) harus tahu persis bahwa Inventory Service ada di port 8081? Bab berikutnya menjawab itu dengan membangun API Gateway sebagai satu pintu masuk tunggal.
Bagian dari Series: Kotlin Backend Microservice - Belajar Fundamental sampai Microservice Nyata
Belajar Kotlin + Spring Boot dari fundamental bahasa (null safety, OOP, coroutines) sampai membangun sistem microservice sungguhan - REST API, Postgre...
Lihat Series Lengkap