Semua yang Saya Ketahui tentang Desain API yang Baik
(seangoedecke.com)- Dalam rekayasa perangkat lunak, API adalah alat inti, dan API yang baik sebaiknya terasa familier dan sederhana sampai-sampai membosankan
- Karena API sulit diubah setelah dipublikasikan, prinsip tidak merusak lingkungan pengguna (WE DO NOT BREAK USERSPACE) sangat penting
- Jika perubahan tak terhindarkan, versioning diperlukan, tetapi ini adalah kejahatan yang perlu karena sangat meningkatkan kompleksitas dan biaya pemeliharaan
- Pada akhirnya, kualitas API bergantung pada nilai produk itu sendiri, dan produk yang dirancang buruk akan menyulitkan pembuatan API yang baik
- Demi stabilitas dan skalabilitas, perlu mempertimbangkan autentikasi berbasis API key, idempotensi, rate limit, dan pagination berbasis cursor
Pendahuluan: pentingnya dan konteks desain API
- Salah satu pekerjaan utama insinyur perangkat lunak modern adalah berinteraksi dengan API
- Penulis juga memiliki pengalaman merancang/mengimplementasikan/memanfaatkan API publik dan internal dalam berbagai bentuk seperti REST, GraphQL, dan alat command line
- Saran desain API yang ada saat ini cenderung terobsesi pada konsep yang rumit (definisi REST, HATEOAS, dan sebagainya)
- Tulisan ini merangkum prinsip desain API yang praktis berdasarkan pengalaman nyata
Keseimbangan antara keakraban dan fleksibilitas: syarat pertama API yang baik
- API yang baik adalah API yang 'biasa dan membosankan', yaitu cara pakainya harus mirip dengan API lain yang sudah pernah ditemui
- Karena pengguna lebih fokus pada pencapaian tujuan mereka daripada API itu sendiri, desain dengan hambatan masuk rendah sangat diperlukan
- API yang sudah dipublikasikan sangat sulit diubah, sehingga kehati-hatian diperlukan sejak tahap desain awal
- Pengembang selalu menginginkan API yang sesederhana mungkin, tetapi tetap harus memikirkan fleksibilitas jangka panjang
- Pada akhirnya, keseimbangan antara keakraban dan fleksibilitas jangka panjang adalah tantangan utamanya
Jangan pernah merusak userspace (WE DO NOT BREAK USERSPACE)
- Perubahan berupa penambahan field pada struktur respons biasanya tidak masalah
- Namun penghapusan field, perubahan tipe, atau perubahan struktur akan merusak kode semua konsumen
- Pemelihara API memiliki tanggung jawab untuk tidak dengan sengaja merusak perangkat lunak pengguna lama
- Bahkan alasan typo pada header HTTP "referer" tidak diperbaiki pun adalah karena budaya menjaga userspace
Mengubah API tanpa merusaknya: strategi versioning
- Perubahan yang breaking pada API hanya boleh dilakukan jika benar-benar perlu, dan dalam situasi itu versioning adalah jawabannya
- Versi lama dan versi baru harus dioperasikan secara bersamaan sambil mendorong transisi bertahap
- Identifier versi bisa diterapkan dengan berbagai cara seperti URL(
/v1/) atau header, sehingga pengguna dapat bermigrasi sesuai kecepatannya masing-masing - Versioning memiliki kelemahan berupa biaya pemeliharaan yang sangat besar (endpoint bertambah, testing, support) dan kebingungan pengguna
- Bahkan jika seperti Stripe ada lapisan translasi internal, kompleksitas dasarnya tetap tidak bisa dihindari
- Penerapan versioning API harus menjadi pilihan terakhir
Faktor keberhasilan API sepenuhnya bergantung pada nilai produk
- API pada dasarnya hanyalah antarmuka dari produk bisnis yang nyata
- API seperti OpenAI dan Twilio pun pada akhirnya dicari pengguna karena fungsi yang disediakan API itu sendiri
- Jika produknya bernilai, orang akan tetap menggunakannya meski API-nya kurang nyaman
- Kualitas API adalah karakteristik "margin": ia baru menjadi faktor pembeda ketika daya saing intinya mirip
- Sebaliknya, produk yang sama sekali tidak punya API adalah hambatan besar bagi pengguna teknis
Jika desain produk buruk, API juga tidak bisa menjadi baik
- Meski ada API yang matang secara teknis, itu tidak berarti jika produknya tidak laku di pasar
- Yang lebih penting, jika struktur resource dasarnya tidak logis atau tidak efisien, hal itu akan terlihat juga di API
- Misalnya, sistem yang menyimpan komentar sebagai linked list akan membuat desain RESTful sekalipun sulit muncul secara alami
- Masalah teknis yang bisa disembunyikan di UI akan terekspos semuanya di API, dan ini memaksa pengguna memahami sistem secara tidak perlu
Autentikasi (Authenticaton) dan keragaman pengguna
- Dukungan untuk autentikasi berbasis API key berumur panjang harus disediakan
- Walaupun metode yang lebih aman seperti OAuth juga didukung, hambatan masuk API key jauh lebih rendah
- Konsumen API bukan hanya engineer, tetapi juga non-developer (sales, perencana, pelajar, pengembang hobi, dan lain-lain)
- Persyaratan autentikasi yang sulit atau rumit (seperti OAuth) menjadi hambatan bagi pengguna nonspesialis
Idempotensi dan penanganan retry
- Untuk request yang bersifat aksi (misalnya pembayaran, perubahan status, dan sebagainya), keamanan terhadap retry saat gagal sangat penting
- Idempotensi berarti menjamin bahwa hasil hanya diproses sekali walaupun request yang sama dikirim berkali-kali
- Cara standarnya adalah mencegah pemrosesan duplikat dengan mengirimkan "idempotency key" melalui parameter atau header
- Penyimpanan idempotency key cukup menggunakan penyimpanan key/value sederhana seperti Redis, dan dalam banyak kasus penerapan masa kedaluwarsa berkala tidak masalah
- Untuk request baca/hapus (gaya REST), ini umumnya tidak diperlukan
Keamanan API dan rate limiting
- Request API melalui kode dapat terjadi jauh lebih cepat daripada interaksi manual pengguna
- Satu API yang dirilis tanpa banyak pikir bisa dimanfaatkan dengan cara tak terduga (misalnya sistem chat skala besar)
- Rate limit wajib ada, dan harus diterapkan lebih ketat pada operasi yang biayanya tinggi
- Opsi menonaktifkan API sementara untuk pelanggan tertentu (
killswitch) juga patut dipertimbangkan - Informasi rate limit perlu disampaikan lewat header respons seperti
X-Limit-Remaining,Retry-After, dan sebagainya
Strategi pagination
- Untuk mengembalikan dataset besar secara efisien (misalnya jutaan tiket), pagination itu wajib
- Pagination berbasis offset (Offset-based) sederhana, tetapi pada data besar akan makin lambat
- Pagination berbasis cursor (Cursor-based) efektif bahkan untuk dataset yang sangat besar tanpa penurunan performa query
- Pendekatan berbasis cursor memang agak lebih sulit diimplementasikan dan digunakan, tetapi dalam jangka panjang kemungkinan besar akan menjadi perubahan yang esensial
- Menyertakan field seperti
next_pagedalam respons agar cursor untuk request berikutnya jelas adalah pilihan yang bijak
Field opsional dan pandangan tentang GraphQL
- Field yang mahal atau lambat sebaiknya dikeluarkan dari respons default dan hanya ditambahkan secara opsional saat diperlukan
- Data terkait bisa disertakan melalui parameter seperti
includes - GraphQL punya keunggulan dalam fleksibilitas struktur data, tetapi juga memiliki masalah seperti aksesibilitas yang lebih rendah bagi non-developer, caching/edge case yang makin rumit, dan kesulitan implementasi di backend
- Berdasarkan pengalaman praktik, penerapan GraphQL sebaiknya dibatasi pada kasus yang benar-benar perlu
Karakteristik API internal
- API internal perusahaan memiliki banyak kondisi yang berbeda dari API eksternal (API yang dipublikasikan)
- Karena konsumennya kebanyakan adalah insinyur perangkat lunak profesional, autentikasi yang lebih kompleks atau perubahan breaking bisa lebih dimungkinkan
- Meski begitu, prinsip desain untuk idempotensi, pencegahan insiden, dan meminimalkan beban operasional tetap berlaku
Ringkasan
- API sulit diubah dan harus mudah digunakan
- Tidak merusak userspace adalah kewajiban paling penting bagi pemelihara API
- Versioning API mahal biayanya sehingga hanya boleh dipakai sebagai pilihan terakhir
- Pada akhirnya, kualitas API ditentukan oleh nilai esensial dari produknya
- Produk yang dirancang buruk memiliki keterbatasan besar meski sudah dicoba ditambal di level API
- Dukungan autentikasi sederhana, idempotensi untuk request aksi yang penting, serta rate limit/pagination dan langkah stabilitas lainnya sangat krusial
- API internal memiliki strategi yang berbeda tergantung tujuan dan penggunanya, tetapi tetap membutuhkan kehati-hatian dalam desain
- REST, JSON, OpenAPI, dan format sejenis bukanlah isu yang paling esensial; dokumentasi yang jelas jauh lebih penting
1 komentar
Komentar Hacker News
Nasihat "jangan pernah merusak userspace" memang terkenal, tetapi poin bagusnya adalah bahwa ada sisi kebalikannya juga. Artinya, "API kernel bisa rusak tanpa pemberitahuan". Yang penting bukan "jangan sembarangan merusak semua API", melainkan keseimbangan yang lebih halus: "jangan pernah merusak hanya bagian yang sudah dinyatakan stabil"
Meski kernel Linux tidak merusak userspace, GNU libc justru sangat sering merusak kompatibilitas userspace. Akibatnya, ruang pengguna Linux tetap sering rusak tidak peduli sekeras apa pun upaya para pengembang kernel. Program dan library yang dibangun dengan libc versi baru kadang tidak bisa berjalan dengan baik di libc yang lebih lama, sehingga pada akhirnya semua komponen harus di-upgrade sekaligus. Agak ironisnya, Windows sudah menyelesaikan masalah ini puluhan tahun lalu dengan pendekatan redistributable
Linux terkenal tidak memiliki API driver publik yang stabil, dan saya dengar inilah yang memotivasi Google mengembangkan Fuschia OS. Linux tampaknya mengambil arah yang berbeda untuk userspace dan untuk hardware
Penulis tampaknya tidak terlalu menyukai API berbasis versi, tetapi saya selalu menyarankan agar versioning diterapkan sejak awal saat membuat aplikasi. Kita tidak bisa memprediksi masa depan, jadi pada suatu saat perubahan yang breaking karena faktor eksternal pasti akan menimpa Anda juga
Saya rasa penulis sebenarnya juga merekomendasikan versioning. Di artikel tertulis bahwa "versioning adalah cara untuk mengubah API secara bertanggung jawab", jadi pada akhirnya ini tetap mendorong versioning itu sendiri. Hanya saja, migrasi ke versi baru sebaiknya menjadi pilihan terakhir
Saya setuju dengan pendapat bahwa tidak perlu menempelkan "v1" di endpoint. Yang biasanya terjadi saat API benar-benar berkembang adalah, mula-mula kita menambahkan field atau opsi ke endpoint lama sambil berusaha menjaga backward compatibility. Lalu, jika dibutuhkan perubahan yang benar-benar tidak kompatibel, biasanya nama endpoint itu sendiri diubah dan dibuat endpoint yang benar-benar baru (bukan /v2). Jika seluruh API harus diubah, layanan lama biasanya dipensiunkan dan diluncurkan layanan baru yang benar-benar berbeda, bahkan dengan nama baru. Dalam 25 tahun bekerja, saya hanya sekali melihat layanan yang memakai "/v1" dan "/v2" berdampingan
Saya rasa maksud penulis bukan bahwa /v1 tidak boleh dimasukkan ke endpoint sejak awal. Intinya adalah kita harus berusaha sekuat mungkin agar /v2 tidak pernah perlu muncul. Begitu /v2 ada, setiap bug fix harus memperbaiki kode di kedua sisi, percabangan kondisi bertambah secara eksponensial, dan codebase menjadi kusut seperti spaghetti. Pada akhirnya, desain /v1 awal yang sampai harus mendukung banyak versi berarti kurang mempertimbangkan kompatibilitas masa depan
Menambahkan versioning belakangan juga menurut saya tidak masalah. Misalnya mulai dulu dengan /api/posts, lalu versi berikutnya cukup ditambahkan sebagai /api/v2/posts
Saya tidak setuju dengan pendekatan menanamkan versi sejak awal. Jika begitu, multi-version justru akan benar-benar sering dipakai, dan menurut saya itu malah lebih buruk
Artikel ini sangat bermanfaat. Saya ingin menambahkan satu nasihat: semakin sulit dokumentasi API diperoleh, semakin buruk kualitas API-nya. Jika Anda harus menandatangani kontrak hanya untuk mendapatkan dokumentasinya, aman untuk berasumsi bahwa kualitas API tersebut buruk
Penulis menyarankan agar idempotency key tidak disimpan di tabel comment, melainkan di key/value store seperti Redis, tetapi saya penasaran apakah pendekatan ini benar-benar bisa menjamin idempotency yang kuat di semua kasus kegagalan. Misalnya, saat server melakukan conditional write seperti
SET key 1 NXlalu menemukan key itu sudah ada, pembuatan komentar seharusnya langsung dilewati, tetapi pada saat itu permintaan sebelumnya mungkin sebenarnya belum tercatat ke DB. Penyimpanan idempotency key harus di-commit bersama pekerjaan aktual dalam unit transaksi yang sama, dan harus bisa di-rollback bila perlu. Pada akhirnya, hakikat idempotency key adalah "ID unik untuk operasi atau request ini". Misalnya harus menjadi identifier per resource yang sesuai untuk masing-masing hal seperti "membuat komentar", "memperbarui komentar", dan seterusnyaKeuntungan cursor-based pagination adalah, dari sudut pandang pengguna, meskipun item baru ditambahkan di antara saat mereka memuat halaman dan menekan tombol "berikutnya", mereka tidak perlu melihat ulang item yang sudah pernah dilihat. Metode cursor mencatat ID objek terakhir dari halaman sebelumnya lalu mengambil item setelah itu, sehingga sangat berguna untuk infinite scroll. Sebaliknya, kelemahan cursor-based adalah fitur "lompat ke halaman ke-N" jadi sulit dibuat
Sekarang kalau orang menyebut "API", kebanyakan yang terbayang adalah mengirim request ke web app, mengatur parameter dan header, lalu mengambil data. Padahal makna asli API adalah "Application Programming Interface", yaitu antarmuka untuk program aplikasi. Istilah ini pertama kali dipakai pada 1940-an, dan hingga 1990-an hampir selalu dipakai tanpa makna lain. Sejarah API sudah lebih dari 80 tahun, dan ada banyak sekali materi lama yang sangat berharga. Jika kita memikirkan masalah apa yang dihadapi para programmer saat itu dan bagaimana mereka menyelesaikannya, kemungkinan ada banyak hal yang tetap berguna bagi kita hari ini
Saya tidak setuju dengan anggapan bahwa pengguna internal cukup dianggap sebagai 'pengguna' saja. Memang mereka biasanya lebih teknis dan kemungkinan besar programmer, tetapi mereka juga sibuk dan fokus pada proyeknya sendiri, sehingga sering kali tidak punya waktu atau kelonggaran untuk menanggapi perubahan API. Kalau bisa, penting untuk melakukan pengujian "dogfooding" (pemakaian nyata) yang cukup di dalam tim sebelum dibuka keluar. Begitu sudah dipublikasikan secara eksternal, janji "jangan merusak userspace" harus benar-benar dijaga
Untuk pengguna internal, biasanya sudah ada alat instrumentasi yang memungkinkan kita menghubungi mereka langsung dan mendorong migrasi. Berkat itu, penghentian versi API juga memungkinkan, jadi menerapkan versioning secara strategis cukup menarik. Saya pernah terlibat langsung dalam versioning API, dan benar-benar melihat hasilnya dibanding organisasi yang pada dasarnya tidak memakai ini
Saya rasa pendekatan versioning memang membantu menyelesaikan masalah ini. Salah satu cara terbaik untuk menghormati pengguna internal adalah berkolaborasi pada spesifikasi, lalu membagikan versi spesifikasi yang sedang dikerjakan kepada para pemangku kepentingan. Bahkan jika dokumentasinya terus diperbarui, keberadaan titik acuan tetap mempermudah umpan balik internal maupun eksternal, dan sangat berguna selama kita menghindari konflik yang murni bersifat kebijakan
Daripada menyimpan idempotency key di redis, jika memungkinkan saya rasa lebih andal menyimpan idempotency key itu dalam transaksi yang sama dengan pencatatan data aktual
Peringatan "jangan pernah merusak userspace" benar-benar penting. Belakangan saya kecewa melihat Spotify, Reddit, Twitter, dan lainnya mengabaikan prinsip ini
Sebagai referensi, rekomendasi praktik API yang bagus juga dirangkum dengan baik di https://jcs.org/2023/07/12/api, jadi saya sarankan untuk melihatnya juga