Jika klien Anda adalah CLI yang dihasilkan ke shell seperti Bash, Ksh, atau Zsh, kontrak API stabil bukan sekadar soal endpoint dan payload. Yang sering membuat integrasi rapuh justru hal-hal di tepi sistem: format output yang berubah, exit code yang tidak konsisten, token yang bocor lewat environment variable, timeout yang tidak jelas, retry yang menggandakan operasi tulis, sampai parsing JSON yang bergantung pada grep atau sed.

Masalah utamanya sederhana: shell sangat kuat untuk otomasi, tetapi juga sangat permisif. Sedikit perubahan pada output atau perilaku error bisa mematahkan pipeline CI, cron job, atau script operasional. Karena itu, API yang dikonsumsi oleh CLI berbasis shell perlu dirancang dengan kontrak yang lebih eksplisit, sempit, dan dapat diuji end-to-end.

Mengapa integrasi shell lebih rapuh daripada klien biasa

Klien berbasis shell sering berada di area yang tidak sepenuhnya terkontrol: dijalankan di mesin developer, server produksi, container minimal, image CI, atau host dengan shell yang berbeda. Di situ, asumsi kecil mudah berubah menjadi bug integrasi.

  • Parser seadanya: banyak script membaca output dengan cut, awk, grep, atau ekspansi shell.
  • Perbedaan shell: perilaku quoting, array, test expression, dan pipeline tidak selalu identik di Bash, Ksh, dan Zsh.
  • Exit code adalah API juga: script sering mengambil keputusan hanya dari status proses, bukan dari body respons.
  • Jaringan tidak andal: timeout, retry, dan partial failure lebih sering muncul di otomasi dibanding penggunaan manual.
  • Lingkungan eksekusi bervariasi: ketersediaan jq, curl, sertifikat CA, proxy, atau locale tidak selalu sama.

Karena itu, stabilitas kontrak tidak boleh dibatasi pada dokumentasi REST. Anda perlu memperlakukan HTTP + JSON + exit code + output CLI sebagai satu kesatuan antarmuka.

Prinsip desain kontrak API stabil untuk CLI

1. Gunakan versioning yang jelas dan konservatif

Untuk klien shell, perubahan kecil bisa berdampak besar. Versioning membantu membatasi blast radius.

  • Versi di path atau header, misalnya /v1/... atau content negotiation yang terdokumentasi jelas.
  • Jangan ubah bentuk field secara diam-diam. Menambah field biasanya aman, mengganti nama field atau mengubah tipe field sering tidak aman.
  • Pisahkan kontrak mesin dan kontrak manusia. Output untuk dibaca manusia boleh berkembang, output untuk diparsing harus stabil.

Praktik aman: sediakan mode output eksplisit seperti --output json pada CLI, lalu definisikan bahwa format JSON tersebut adalah bagian dari kontrak versi.

2. Tetapkan schema respons yang eksplisit

CLI yang baik tidak menebak-nebak bentuk data. Respons API juga tidak boleh ambigu.

Contoh respons yang relatif aman untuk diproses:

{
  "data": {
    "job_id": "job_123",
    "status": "queued"
  },
  "error": null,
  "request_id": "req_abc"
}

Beberapa aturan praktis:

  • Gunakan field yang konsisten, misalnya selalu ada data, error, dan request_id.
  • Jangan mengembalikan kadang object, kadang string, untuk field yang sama.
  • Bedakan null, string kosong, dan field yang tidak ada. Tiga hal ini punya implikasi parsing berbeda.
  • Untuk daftar, selalu kembalikan array, meski kosong.

Schema eksplisit memudahkan validasi di sisi server, pengujian kontrak, dan penanganan error yang konsisten di CLI.

3. Definisikan model error yang konsisten

Salah satu sumber bug terbesar adalah API yang sukses dan gagal dengan bentuk respons berbeda total. Klien shell akhirnya membuat cabang parsing yang rapuh.

Model error yang baik sebaiknya memuat:

  • kode error stabil yang dapat diproses mesin, misalnya INVALID_TOKEN, RATE_LIMITED, CONFLICT
  • pesan manusia untuk debugging
  • request_id untuk korelasi log
  • detail opsional untuk validasi field atau retry policy
{
  "data": null,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests",
    "retryable": true
  },
  "request_id": "req_def"
}

Dengan pola ini, CLI bisa menentukan apakah perlu menampilkan pesan, melakukan retry, atau langsung gagal dengan exit code tertentu.

Pitfall integrasi yang paling sering muncul

Perubahan format output CLI

Kesalahan umum adalah mencampur output manusia dan output mesin pada stream yang sama. Misalnya, CLI menampilkan progress, warning, dan JSON di stdout. Script lalu gagal karena parser menerima teks tambahan.

Pola yang lebih aman:

  • stdout hanya untuk data utama yang akan diproses mesin.
  • stderr untuk log, warning, progress, dan pesan error manusia.
  • Sediakan mode eksplisit: --output json, --quiet, --no-color.

Jangan mengubah format output default tanpa mempertimbangkan script yang sudah ada. Bahkan menambahkan satu baris banner dapat merusak pipeline.

Exit code yang tidak terdefinisi jelas

Di shell, exit code adalah kontrak inti. Banyak orchestrator hanya melihat status proses.

Minimal, dokumentasikan peta berikut:

  • 0: sukses
  • >0: gagal
  • kode khusus untuk kategori penting, misalnya autentikasi gagal, input tidak valid, timeout, atau kesalahan jaringan

Yang penting bukan banyaknya kode, melainkan konsistensinya. Jika semua kegagalan dikembalikan sebagai 1, script tidak bisa membedakan apakah perlu retry atau intervensi manusia.

Jika Anda memang membutuhkan pembedaan, jaga agar kategorinya stabil. Contohnya:

  • kesalahan penggunaan CLI: argumen salah, file tidak ada
  • kesalahan remote permanen: token salah, resource tidak ditemukan
  • kesalahan remote sementara: timeout, 429, 503

Timeout dan retry yang tidak terkendali

Default timeout yang tidak jelas membuat script menggantung. Retry otomatis tanpa batas juga berbahaya: bisa memperlambat pipeline atau memperbanyak beban server.

Rekomendasi praktis:

  • Tetapkan connect timeout dan read timeout secara eksplisit.
  • Retry hanya untuk error yang kemungkinan sementara, seperti timeout, reset koneksi, 429, atau 5xx tertentu.
  • Gunakan backoff dan, bila mungkin, jitter.
  • Jangan retry operasi tulis yang tidak idempotent tanpa mekanisme pengaman.

Untuk shell, pendekatan sederhana sudah cukup: jumlah retry kecil, jeda bertahap, dan batas waktu total. Yang penting dapat diprediksi.

Idempotency untuk operasi tulis

Ini sering diabaikan. Script shell kerap dijalankan ulang karena timeout, sinyal, atau kegagalan CI. Jika operasi tulis seperti membuat order, membuat deployment, atau menagih pembayaran tidak idempotent, retry bisa menggandakan efek samping.

Solusi yang aman adalah menggunakan idempotency key untuk operasi create atau action yang sensitif. Kunci tersebut dikirim bersama request, lalu server menjamin request yang sama tidak dieksekusi dua kali secara semantis.

POST /v1/jobs
Idempotency-Key: 8b2f1c4e-... 
Content-Type: application/json

{
  "task": "deploy",
  "target": "api"
}

Beberapa hal penting:

  • Kunci harus unik per aksi logis, bukan per percobaan jaringan.
  • Server perlu menyimpan hasil atau status request berdasarkan kunci dalam jangka waktu tertentu.
  • Dokumentasikan kapan respons lama akan dikembalikan, dan apa yang terjadi bila payload berbeda tetapi kuncinya sama.

Tanpa ini, retry di sisi CLI berpotensi menciptakan duplikasi yang sulit dilacak.

Auth token di environment variable

Environment variable adalah cara praktis, tetapi bukan tanpa risiko. Token bisa ikut tercetak di log debug, terbaca oleh proses lain tergantung lingkungan, atau tidak sengaja tersimpan di shell history jika dipasang langsung pada command line.

Praktik yang lebih aman:

  • Baca token dari environment variable yang terdokumentasi, misalnya API_TOKEN.
  • Jangan tampilkan token di log, termasuk pada mode verbose.
  • Hindari meminta pengguna menaruh token langsung di argumen command karena rawan bocor ke riwayat shell atau daftar proses.
  • Jika perlu, dukung file credential dengan permission ketat sebagai alternatif.
API_TOKEN='...' mycli job create --output json

Jika CLI memanggil curl atau tool lain, pastikan token tidak ikut diekspansi ke log yang dibuka pengguna.

Quoting dan escaping

Masalah quoting adalah sumber bug klasik di shell. Payload JSON, header HTTP, dan argumen yang mengandung spasi, newline, kutip tunggal, atau karakter shell dapat rusak sebelum sampai ke server.

Kesalahan umum:

  • Menyisipkan JSON mentah langsung dalam string shell kompleks.
  • Mengandalkan interpolasi tanpa memahami kapan shell melakukan ekspansi.
  • Menganggap perilaku sama di Bash, Ksh, dan Zsh.

Pola yang lebih aman:

  • Jika memungkinkan, kirim body dari file atau stdin, bukan membangun JSON inline yang rumit.
  • Jaga semua variabel tetap ter-quote dengan benar.
  • Uji payload yang mengandung spasi, kutip, backslash, dan newline.
payload_file=$(mktemp)
cat > "$payload_file" <<'JSON'
{
  "task": "deploy",
  "note": "rollback if health check fails"
}
JSON

curl -sS \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @"$payload_file" \
  https://api.example.com/v1/jobs

Pendekatan ini lebih panjang, tetapi jauh lebih stabil daripada merangkai JSON dalam satu string shell.

Parsing JSON yang rapuh

Jangan parse JSON dengan grep, cut, atau regex kecuali formatnya benar-benar terkendali dan sangat sederhana. JSON memiliki escaping, nested object, array, dan whitespace yang mudah mematahkan parser ad-hoc.

Jika lingkungan memungkinkan, gunakan parser JSON yang memang dibuat untuk itu. Jika tidak, pertimbangkan dua strategi:

  • Sediakan output yang memang stabil untuk shell, misalnya field tunggal dengan format key=value yang terdokumentasi.
  • Atau pastikan dependensi parser JSON menjadi bagian eksplisit dari kontrak operasional CLI.

Kesalahan desain yang sering terjadi adalah mengklaim output JSON stabil, tetapi kemudian CLI menambahkan banner, warning, atau progress ke stream yang sama.

Kompatibilitas lintas shell

Jika CLI menghasilkan script atau glue code untuk Bash, Ksh, dan Zsh, jangan bergantung pada fitur spesifik salah satu shell kecuali memang dinyatakan sebagai syarat.

Hal yang perlu diwaspadai:

  • Array dan associative array tidak seragam antar-shell.
  • Perilaku echo terhadap escape sequence tidak selalu konsisten; printf biasanya lebih aman.
  • set -e dan pipeline dapat memiliki perilaku mengejutkan, terutama saat command gagal di subshell atau dalam kondisi tertentu.
  • Word splitting dan globbing dapat mengubah argumen jika variabel tidak di-quote.

Jika targetnya lintas shell, prioritaskan subset POSIX atau dokumentasikan dengan tegas shell yang didukung. Semakin sempit asumsi Anda, semakin kuat kontraknya.

Pola desain API yang aman untuk otomasi shell

Request ID dan korelasi log

Setiap respons sebaiknya memuat request_id. Ini sangat membantu saat script gagal di CI atau mesin pengguna dan Anda perlu menelusuri log server.

Lebih baik lagi jika server juga menerima header korelasi dari klien, lalu mengembalikannya bila valid. Dengan begitu, satu eksekusi CLI bisa dilacak lintas hop.

Webhook vs polling

Untuk operasi asinkron, memilih antara webhook dan polling memengaruhi kesederhanaan CLI.

Polling biasanya lebih mudah untuk shell karena tidak memerlukan endpoint inbound. Namun polling perlu kontrak status yang jelas:

  • status yang eksplisit, misalnya queued, running, succeeded, failed
  • interval polling yang direkomendasikan
  • batas waktu atau kondisi terminal yang terdokumentasi

Webhook lebih efisien jika Anda memang punya layanan penerima callback. Namun untuk script shell murni, webhook sering tidak praktis.

Untuk banyak kasus CLI, pola aman adalah:

  1. request membuat job dan mengembalikan job_id
  2. CLI melakukan polling ke endpoint status
  3. status terminal menentukan exit code akhir

Ini lebih mudah diuji dan lebih cocok untuk lingkungan shell.

Bedakan operasi yang aman untuk retry dan yang tidak

API sebaiknya membantu klien membedakan apakah retry aman. Misalnya melalui:

  • metode HTTP yang semestinya aman atau idempotent bila sesuai
  • kode error stabil
  • field seperti retryable pada body error
  • dokumentasi eksplisit tentang side effect

CLI tidak seharusnya menebak dari pesan teks.

Contoh kontrak yang lebih tahan terhadap script shell

Berikut contoh alur yang lebih aman untuk operasi pembuatan job asinkron:

POST /v1/jobs
Authorization: Bearer <token>
Idempotency-Key: <uuid>
Content-Type: application/json

{
  "task": "deploy",
  "target": "api"
}

Respons sukses awal:

{
  "data": {
    "job_id": "job_123",
    "status": "queued"
  },
  "error": null,
  "request_id": "req_001"
}

Polling status:

GET /v1/jobs/job_123
Authorization: Bearer <token>

Respons terminal gagal:

{
  "data": {
    "job_id": "job_123",
    "status": "failed"
  },
  "error": {
    "code": "DEPLOYMENT_FAILED",
    "message": "Health check did not pass",
    "retryable": false
  },
  "request_id": "req_002"
}

Dari kontrak ini, CLI dapat menerapkan aturan sederhana:

  • jika request create timeout, ulangi dengan idempotency key yang sama
  • jika status job terminal sukses, keluar dengan code 0
  • jika gagal permanen, tampilkan ringkasan ke stderr dan keluar dengan code non-zero yang konsisten
  • selalu log request_id untuk troubleshooting

Checklist pengujian kontrak end-to-end

Dokumentasi saja tidak cukup. Untuk klien shell, pengujian kontrak end-to-end sangat penting karena banyak bug muncul dari interaksi antarlapis, bukan dari API atau CLI secara terpisah.

Uji bentuk output

  • Pastikan mode --output json menghasilkan JSON valid tanpa noise di stdout.
  • Pastikan warning, progress, dan debug masuk ke stderr.
  • Uji kompatibilitas dengan parser yang benar-benar dipakai di script.

Uji exit code

  • Sukses sinkron dan asinkron
  • Input tidak valid
  • 401/403 autentikasi dan otorisasi
  • 404 resource tidak ada
  • 429 atau throttling
  • timeout jaringan dan DNS failure

Pastikan setiap skenario menghasilkan exit code yang sesuai dan stabil.

Uji retry dan idempotency

  • Simulasikan timeout setelah request dikirim tetapi sebelum respons diterima.
  • Verifikasi bahwa retry dengan idempotency key yang sama tidak menggandakan operasi.
  • Uji respons saat key sama dikirim dengan payload berbeda.

Uji quoting dan karakter khusus

  • Spasi, kutip tunggal, kutip ganda, backslash
  • newline dalam field teks
  • karakter wildcard shell seperti * dan ?
  • nilai yang dimulai dengan tanda minus

Uji lintas shell

  • Jalankan test di Bash, Ksh, dan Zsh bila memang semuanya didukung.
  • Gunakan environment minimal untuk mendeteksi dependensi tersembunyi.
  • Uji dengan locale dan opsi shell yang berbeda bila relevan.

Uji keamanan operasional

  • Pastikan token tidak muncul di log normal maupun verbose.
  • Pastikan file sementara dibersihkan.
  • Pastikan respons error tidak memicu kebocoran data sensitif ke terminal atau CI logs.

Kesalahan desain yang sebaiknya dihindari

  • Menganggap output untuk manusia bisa diparse dengan aman.
  • Menambahkan field atau banner ke stream yang sama tanpa mode output eksplisit.
  • Retry semua POST secara otomatis tanpa idempotency key.
  • Menggunakan pesan error bebas sebagai satu-satunya sinyal logika.
  • Mengunci implementasi pada Bash padahal mengklaim dukungan lintas shell.
  • Menyimpan token di argumen command sehingga mudah bocor ke history atau daftar proses.

Penutup

Merancang kontrak API stabil untuk CLI yang dihasilkan ke shell berarti menganggap shell sebagai lingkungan integrasi yang keras, bukan sekadar wrapper tipis. Stabilitas tidak cukup dijaga di level endpoint; Anda juga harus mendefinisikan output mesin, exit code, timeout, retry, idempotency, autentikasi, dan perilaku error secara eksplisit.

Jika harus memilih prioritas, fokuslah pada hal-hal berikut: versioning yang disiplin, schema respons yang konsisten, model error yang dapat diproses mesin, idempotency key untuk operasi tulis, pemisahan stdout/stderr, dan pengujian kontrak end-to-end lintas shell. Dengan itu, script otomasi Anda akan jauh lebih tahan terhadap perubahan dan kegagalan jaringan yang nyata.