Spec Driven Development Part 3: Spec untuk API & Kontrak Data
Part 2 membahas cara menulis spec untuk fitur — intent, constraint, acceptance criteria, dan non-goals yang ditujukan untuk satu potongan pekerjaan yang dikerjakan sekali. Tapi ada satu kategori spec yang punya karakteristik berbeda: spec untuk API dan skema data. Bedanya bukan di format, tapi di konsekuensi. Sebuah spec fitur yang ambigu paling buruk menghasilkan implementasi yang salah dan perlu direvisi. Sebuah kontrak API yang ambigu bisa membuat frontend dan backend membangun asumsi berbeda secara paralel, atau lebih buruk, membuat breaking change yang merusak klien eksternal yang bahkan tidak kamu tahu keberadaannya. Artikel ini membahas cara menulis spec untuk API dan data sebagai kontrak yang benar-benar executable — bisa divalidasi otomatis, bukan sekadar dokumentasi yang dibaca lalu diabaikan.
Kenapa Kontrak Data Berbeda dari Spec Fitur Biasa
Spec fitur biasa punya satu “konsumen” utama: agent atau developer yang mengimplementasikan fitur tersebut. Begitu fitur selesai dan terverifikasi, spec itu sudah menyelesaikan tugasnya — meskipun tetap berguna sebagai dokumentasi historis.
Kontrak API dan skema data berbeda karena punya banyak konsumen yang bekerja secara paralel dan independen:
- Tim frontend yang membangun UI berdasarkan asumsi bentuk response
- Tim backend lain yang mengonsumsi API ini sebagai bagian dari service mereka
- Klien eksternal atau partner yang mengintegrasikan sistem mereka dengan API ini
- AI agent yang menghasilkan kode di kedua sisi — kadang agent yang sama menulis server dan client, kadang agent berbeda yang tidak saling tahu asumsi satu sama lain
Karena banyak pihak bergantung pada kontrak yang sama, ambiguitas di sini jauh lebih mahal. Kalau spec fitur reset password dari Part 2 ambigu soal “berapa lama token kedaluwarsa”, dampaknya terbatas pada satu fitur. Kalau spec API ambigu soal “apakah field email ini selalu ada di response atau bisa null”, dampaknya menyebar ke setiap kode yang pernah mengonsumsi endpoint tersebut — dan begitu sudah dipakai banyak pihak, memperbaikinya menjadi breaking change yang harus dikoordinasikan, bukan sekadar revisi internal.
Inilah kenapa kontrak data butuh format spec yang lebih ketat dan, idealnya, bisa divalidasi oleh tooling — bukan hanya dibaca manusia.
Spec fitur yang ambigu menghasilkan revisi. Spec API yang ambigu menghasilkan integrasi yang gagal di production, kadang berbulan-bulan setelah kontraknya ditulis, ketika konsumen baru muncul dengan asumsi berbeda dari yang dimaksud penulis spec.
OpenAPI sebagai Spec yang Executable
Untuk REST API, OpenAPI (sebelumnya dikenal sebagai Swagger) adalah format yang paling matang untuk menulis kontrak yang executable. Bedanya dengan dokumentasi API biasa: OpenAPI ditulis dalam format terstruktur (YAML atau JSON) yang bisa diparsing oleh tooling, bukan prosa bebas yang hanya bisa dibaca manusia.
Ada tiga keuntungan praktis menjadikan OpenAPI sebagai spec, bukan hasil akhir:
Validasi otomatis. Request dan response bisa divalidasi terhadap schema secara otomatis di test maupun di runtime. Kalau implementasi menyimpang dari kontrak, validasi akan gagal — tidak perlu menunggu reviewer manusia menyadari ketidaksesuaian.
Generate stub dan client. Dari satu file OpenAPI, bisa digenerate server stub, client SDK, bahkan mock server untuk testing — semuanya konsisten karena berasal dari sumber yang sama.
Dokumentasi yang tidak pernah basi. Karena dokumentasi diturunkan dari spec yang sama dengan yang divalidasi, dokumentasi tidak bisa “lupa diupdate” seperti README yang ditulis manual terpisah dari kode.
Yang membuat OpenAPI relevan secara khusus untuk SDD: agent bisa membaca file OpenAPI dan langsung memahami bentuk request/response yang diharapkan, tanpa perlu menebak dari membaca kode existing yang mungkin sudah tidak konsisten.
JSON Schema untuk Validasi Data
OpenAPI sendiri menggunakan JSON Schema di balik layar untuk mendefinisikan bentuk request dan response body. Tapi JSON Schema juga berguna sebagai spec tersendiri — misalnya untuk struktur data yang disimpan di database, payload event/message queue, atau konfigurasi yang dibaca aplikasi.
Prinsip dasarnya: setiap field harus didefinisikan tipe, apakah required, dan constraint nilainya — bukan dibiarkan implisit.
{
"type": "object",
"required": ["id", "email", "status", "createdAt"],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"email": {
"type": "string",
"format": "email",
"maxLength": 255
},
"status": {
"type": "string",
"enum": ["pending", "active", "suspended", "deleted"]
},
"displayName": {
"type": ["string", "null"],
"maxLength": 100
},
"createdAt": {
"type": "string",
"format": "date-time"
}
},
"additionalProperties": false
}
Perhatikan beberapa detail yang sengaja eksplisit di sini:
requiredmenyatakan field mana yang wajib ada — agent tidak perlu menebak apakahemailbisa hilang dari responseenumpadastatusmencegah agent (atau implementasi manapun) menambahkan nilai status baru yang tidak terdokumentasidisplayNamesecara eksplisit dinyatakan bolehnull, berbeda dari field lain yang tidak boleh — perbedaan ini sering jadi sumber bug kalau hanya diasumsikanadditionalProperties: falsemelarang field tambahan yang tidak terdaftar, mencegah schema “mengembang” diam-diam seiring waktu
Schema yang membiarkanadditionalPropertiesdefault (yang berartitrue) terlihat fleksibel, tapi ini membuka pintu bagi field tak terduga menyelinap masuk tanpa validasi. Untuk kontrak yang harus stabil, eksplisit melarang field tambahan jauh lebih aman daripada mengasumsikan semua pihak akan disiplin.
Protobuf untuk Kontrak Antar Service
Untuk komunikasi antar service di dalam sistem sendiri — terutama yang sensitif terhadap performa atau butuh strong typing yang lebih ketat dari JSON — Protocol Buffers (Protobuf) sering jadi pilihan yang lebih cocok dibanding REST plus JSON.
Bedanya dengan OpenAPI/JSON Schema bukan soal mana yang “lebih baik” secara umum, tapi soal konteks pemakaian:
| Aspek | REST + OpenAPI/JSON | gRPC + Protobuf |
|---|---|---|
| Konsumen tipikal | Klien eksternal, browser, partner API | Komunikasi internal antar service |
| Tipe data | Fleksibel, validasi di runtime | Strict, divalidasi saat compile |
| Performa | Lebih besar payload (JSON text-based) | Lebih kecil dan cepat (binary) |
| Evolusi schema | Manual, butuh disiplin versioning | Built-in (field number, reserved keyword) |
| Tooling generate kode | Tersedia luas, banyak bahasa | Native, terintegrasi erat dengan gRPC |
Protobuf punya keunggulan khusus untuk SDD: skema .proto itu sendiri adalah kontrak yang dipakai untuk generate kode di kedua sisi (client dan server), di banyak bahasa sekaligus. Tidak ada celah antara “spec” dan “implementasi” karena keduanya diturunkan dari file yang sama.
syntax = "proto3";
message ResetPasswordRequest {
string email = 1;
}
message ResetPasswordResponse {
bool accepted = 1;
string message = 2;
}
message ConfirmResetRequest {
string token = 1;
string new_password = 2;
}
message ConfirmResetResponse {
bool success = 1;
string error_code = 2; // kosong jika success = true
}
service PasswordResetService {
rpc RequestReset(ResetPasswordRequest) returns (ResetPasswordResponse);
rpc ConfirmReset(ConfirmResetRequest) returns (ConfirmResetResponse);
}
Untuk tim yang belum butuh kompleksitas gRPC, REST dengan OpenAPI tetap pilihan yang masuk akal sebagai default. Pertimbangkan Protobuf ketika komunikasi terjadi murni antar service internal dengan volume tinggi, atau ketika strong typing lintas bahasa menjadi prioritas.
flowchart TD
A{Siapa konsumen kontrak?} -- Klien eksternal/browser --> B[REST + OpenAPI]
A -- Service internal --> C{Volume traffic tinggi atau perlu strong typing lintas bahasa?}
C -- Ya --> D[gRPC + Protobuf]
C -- Tidak --> BMenulis Spec API: Contoh End-to-End
Melanjutkan contoh fitur reset password dari Part 2, berikut bagaimana acceptance criteria yang sudah ditulis di sana diterjemahkan menjadi kontrak OpenAPI yang konkret.
openapi: 3.0.3
info:
title: Password Reset API
version: 1.0.0
paths:
/api/v1/password-reset/request:
post:
summary: Mengajukan permintaan reset password
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
responses:
"200":
description: >
Permintaan diterima. Response sama persis baik email terdaftar
maupun tidak, untuk mencegah email enumeration.
content:
application/json:
schema:
type: object
required: [accepted, message]
properties:
accepted:
type: boolean
enum: [true]
message:
type: string
"429":
description: Rate limit terlampaui (maksimal 3 request per email per jam)
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/api/v1/password-reset/confirm:
post:
summary: Mengonfirmasi reset password dengan token
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token, newPassword]
properties:
token:
type: string
format: uuid
newPassword:
type: string
minLength: 8
responses:
"200":
description: Password berhasil diubah
content:
application/json:
schema:
type: object
required: [success]
properties:
success:
type: boolean
enum: [true]
"410":
description: Token sudah kedaluwarsa atau sudah pernah dipakai
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"422":
description: Password baru tidak memenuhi kebijakan validasi
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
schemas:
ErrorResponse:
type: object
required: [errorCode, message]
properties:
errorCode:
type: string
message:
type: string
Setiap status code di sini bukan kebetulan — masing-masing terhubung langsung ke acceptance criteria yang sudah ditulis di Part 2. HTTP 410 untuk token kedaluwarsa, HTTP 429 untuk rate limit, HTTP 422 untuk validasi password — semua eksplisit, sehingga agent yang mengimplementasikan endpoint ini, dan agent atau developer lain yang mengonsumsinya, punya pemahaman yang sama persis tentang bagaimana setiap kasus harus ditangani.
Alur interaksi lengkap antara client, API, dan email service untuk kedua endpoint ini terlihat seperti berikut:
sequenceDiagram
participant Client
participant API
participant DB
participant EmailService
Client->>API: POST /password-reset/request {email}
API->>DB: Simpan token (hash) + expiry 15 menit
API->>EmailService: Kirim email berisi link reset
API-->>Client: 200 {accepted: true}
Client->>API: POST /password-reset/confirm {token, newPassword}
API->>DB: Validasi token (exists, belum expired, belum dipakai)
alt Token valid
API->>DB: Update password, hapus token
API-->>Client: 200 {success: true}
else Token expired/used
API-->>Client: 410 ErrorResponse
else Password tidak valid
API-->>Client: 422 ErrorResponse
endDefinisikan skema error response secara terpusat (seperti ErrorResponse di atas) dan pakai ulang di semua endpoint. Ini mencegah setiap endpoint punya format error yang berbeda-beda — masalah umum ketika beberapa endpoint dibangun oleh agent atau developer berbeda tanpa kontrak bersama.Versioning dan Backward Compatibility sebagai Constraint
Salah satu constraint yang paling sering hilang dari spec API adalah kebijakan versioning — bagaimana kontrak boleh berubah seiring waktu tanpa merusak konsumen yang sudah ada.
Tanpa kebijakan eksplisit, agent yang diminta “menambah field baru” atau “mengubah validasi” tidak punya panduan apakah perubahan itu aman dilakukan langsung di endpoint existing, atau harus lewat versi baru. Beberapa aturan yang sebaiknya dinyatakan eksplisit di level spec, bukan diasumsikan:
Constraint Versioning:
- Menambah field baru yang optional pada response: AMAN, tidak perlu
versi baru
- Menghapus field dari response: BREAKING, wajib versi baru
- Mengubah tipe data field yang sudah ada: BREAKING, wajib versi baru
- Mengubah field dari optional menjadi required pada request: BREAKING,
wajib versi baru
- Menambah endpoint baru: AMAN, tidak perlu versi baru
- Mengubah perilaku endpoint existing (meski signature tidak berubah):
BREAKING, wajib versi baru atau feature flag
Aturan semacam ini bisa dianggap sebagai “constraint global” yang berlaku di semua spec API dalam satu project — tidak perlu ditulis ulang di setiap spec endpoint, tapi harus didokumentasikan sekali di level project (misalnya di file konvensi yang dibaca semua agent sebelum bekerja) dan dirujuk dari tiap spec individual.
Untuk API yang sudah punya konsumen eksternal, pertimbangkan juga menyatakan masa hidup minimum versi lama secara eksplisit:
Constraint Lifecycle:
- Versi API lama harus tetap didukung minimal 6 bulan setelah versi baru
dirilis
- Endpoint yang deprecated harus mengembalikan header
Deprecation: true dan Sunset: <tanggal>
- Breaking change tidak boleh dirilis tanpa pengumuman minimal 30 hari
sebelumnya ke konsumen terdaftar
Anti-Pattern dalam Spec API
Beberapa pola yang sering muncul dan melemahkan kontrak yang seharusnya ketat:
Schema yang terlalu longgar. Semua field ditandai optional, atau tipe data dibiarkan any/object tanpa struktur jelas. Ini terlihat fleksibel tapi sebenarnya memindahkan beban validasi ke setiap konsumen, yang masing-masing akan menebak dengan caranya sendiri.
ANTI-PATTERN:
properties:
data:
type: object
description: "Data response, struktur bervariasi"
BENAR:
properties:
data:
type: object
required: [id, status]
properties:
id:
type: string
format: uuid
status:
type: string
enum: [pending, completed, failed]
Tidak mendefinisikan error response. Banyak spec API hanya mendefinisikan response sukses dan mengabaikan bentuk error, padahal dari sisi konsumen, menangani error dengan benar sama pentingnya dengan menangani sukses. Tanpa skema error yang jelas, setiap endpoint cenderung mengembalikan format error yang berbeda-beda.
Tidak menyatakan kebijakan versioning. Seperti dibahas di atas — tanpa aturan eksplisit, breaking change bisa masuk tanpa sengaja, terutama ketika beberapa agent atau developer bekerja di endpoint yang sama pada waktu berbeda.
Mendokumentasikan perilaku, bukan kontrak. Spec yang menjelaskan “endpoint ini melakukan X, Y, Z secara internal” alih-alih mendefinisikan bentuk request/response yang harus dipatuhi. Detail implementasi internal sebaiknya tidak masuk ke kontrak API — kontrak hanya peduli pada apa yang terlihat dari luar (input, output, error), bukan bagaimana itu dicapai di dalam.
Kontrak API yang baik adalah kontrak yang tetap valid meskipun implementasi internalnya ditulis ulang total. Jika perubahan implementasi internal memaksa kontrak ikut berubah, kemungkinan kontrak itu bocor terlalu banyak detail internal ke permukaan publiknya.
Ringkasan
- Kontrak API dan skema data berbeda dari spec fitur biasa karena dikonsumsi banyak pihak secara paralel — ambiguitas di sini jauh lebih mahal untuk diperbaiki setelah dipakai luas
- OpenAPI menjadikan kontrak REST API executable: bisa divalidasi otomatis, generate stub/client, dan dokumentasi yang selalu sinkron dengan spec
- JSON Schema mendefinisikan struktur data secara presisi —
required,enum, danadditionalProperties: falsemencegah ambiguitas tentang field mana yang wajib dan boleh berubah- Protobuf lebih cocok untuk komunikasi antar service internal yang butuh performa dan strong typing lintas bahasa, dibanding REST+JSON yang lebih cocok untuk klien eksternal
- Setiap status code dan error response dalam spec API sebaiknya terhubung langsung ke acceptance criteria yang sudah didefinisikan di spec fitur
- Kebijakan versioning harus dinyatakan eksplisit sebagai constraint — perubahan apa yang aman dilakukan langsung dan apa yang wajib lewat versi baru
- Hindari schema yang terlalu longgar (semua optional/any type), error response yang tidak terdefinisi, dan kebocoran detail implementasi internal ke dalam kontrak publik