# Belajar Desain API untuk Pemula: Resource, Error, Versioning

> Belajar desain API untuk pemula: resource dan method, status code yang jujur, satu format error, versioning, deprecation, rate limit, pagination, OpenAPI.

URL: https://www.matthewswong.com/id/learn/system-design/api-design-for-beginners/

System design langkah demi langkah · Pelajaran 8 dari 11 · Ditinjau pada 18 Oktober 2026 · Oleh Matthews Wong (https://www.matthewswong.com/id/about/)

> Desain API adalah menentukan kontrak antara server dan program yang memanggilnya: resource apa saja yang ada, method apa yang bekerja padanya, arti setiap status code, dan bentuk sebuah error. API pertama yang baik menamai resource dengan kata benda, memakai method sesuai maknanya di HTTP, mengembalikan satu bentuk error dengan code yang stabil, menaruh versi di path, dan memublikasikan seluruh kontraknya sebagai OpenAPI.

## Setelah pelajaran ini, Anda bisa

- Memodelkan endpoint sebagai resource plus method, lalu memilih status code yang memberi tahu client mana pun apa yang terjadi.
- Merancang satu envelope error dengan code yang stabil dan hint, lalu membandingkannya dengan problem details RFC 9457.
- Memensiunkan endpoint dengan header Deprecation, Link, dan Sunset, serta memublikasikan kontraknya sebagai OpenAPI.

## Satu client bertemu kontrak API

Sebuah client membaca kontrak di /openapi.json, memanggil path lama tanpa versi yang masih berfungsi tetapi menjawab dengan header Deprecation dan Link, lalu bertemu pemeriksaan method, rate limit, dan validasi di endpoint v1, tempat setiap kegagalan keluar dalam envelope error yang sama dan menunjuk balik ke kontrak.

1. **Kenali bagiannya.** Sebuah client, kontrak API di /openapi.json, path lama tanpa versi, dan v1. Request ke v1 melewati rate limit, lalu validasi, dan setiap kegagalan keluar dalam satu envelope error.
2. **Baca kontraknya dulu.** Client memulai dari kontrak. GET /openapi.json mengembalikan setiap path, method-nya, field yang wajib, dan skema error, yang dibangun dari konstanta yang sama dengan yang ditegakkan route.

   ```
   openapi: "3.1.0",
   ```

   *Tebak:* Client lama masih memanggil /api/contact/, path tanpa versi. Apa yang didapatnya? (404, karena path lamanya sudah dihapus / Jawaban biasa, ditambah header Deprecation / Redirect ke /api/v1/contact/)
3. **Path lama, header baru.** Path lama tetap menjawab lewat handler yang sama: deprecation tidak mengubah perilaku apa pun. Setiap response menambahkan Deprecation, tanggal kejadiannya, dan Link yang successor-version-nya menunjuk ke v1.

   ```
   deprecation: @1791417600
   rel="successor-version"
   ```
4. **Method salah: 405.** GET ke v1 mendapat 405 Method Not Allowed. RFC 9110 mewajibkan header Allow yang mendaftar method yang berlaku, di sini POST, dan body-nya tetap envelope error yang sama.

   ```
   status: 405,
   code: "method_not_allowed",
   ```

   *Tebak:* Berikutnya client mengirim POST berisi objek JSON kosong. Bagaimana kode client membedakan kegagalan ini dari rate limit? (Lewat field code yang stabil / Dengan mem-parsing pesan error)
5. **Validasi menyebut field-nya.** Objek JSON kosong lolos rate limit, lalu gagal validasi: 400, code invalid_request, dan hint yang menyebut field-nya. Program bercabang berdasarkan code; manusia membaca error dan hint.

   ```
   code: "invalid_request",
   hint: "`name` must be 2 to 100 characters.",
   ```
6. **Terlalu banyak: 429.** Setelah lima POST dalam semenit, POST keenam berhenti di rate limit: 429, header Retry-After dalam detik, dan code rate_limited. Envelope-nya sama, tidak perlu parser kedua.

   ```
   status: 429,
   code: "rate_limited",
   "Retry-After": String(resetIn),
   ```
7. **Setiap error menunjuk pulang.** Setiap error membawa docs dan header Link dengan rel service-desc, keduanya menunjuk ke /openapi.json. Client yang hanya pernah melihat error tetap bisa menemukan seluruh kontraknya.

   ```
   docs: API_DOCS_URL,
   ```

## Apa itu desain API?

Desain API adalah menentukan kontrak antara server dan program yang memanggilnya, sebelum ada yang menulis client. Kontrak itu menyebut resource yang tersedia, method yang diterima masing-masing, bentuk setiap request dan response, status code, dan rupa sebuah error. Pemanggil tidak bisa membaca source code Anda, jadi kontrak itulah satu-satunya pegangannya, dan apa pun yang Anda publikasikan bisa mulai diandalkan oleh pemanggil.

Rancang dari sisi pemanggil, dimulai dengan siapa pemanggilnya: browser, aplikasi mobile, server milik partner, service Anda sendiri, atau AI agent. API situs ini kecil, hanya tiga endpoint POST untuk asisten chat dan form kontak, tetapi sifatnya publik, jadi pemanggilnya adalah orang asing. Karena itulah usahanya dicurahkan pada bentuk error yang bisa ditebak, versi di setiap path, dan deskripsi yang dipublikasikan di /openapi.json.

## Bagaimana resource dan method HTTP saling melengkapi?

Di API bergaya REST, URL menamai sebuah benda dan method menyebut apa yang dilakukan terhadapnya. Pakai kata benda untuk resource, /orders untuk koleksinya dan /orders/42 untuk satu order, lalu biarkan method yang membawa kata kerjanya. RFC 9110 mendefinisikan makna setiap method. GET, HEAD, OPTIONS, dan TRACE bersifat safe: client tidak meminta perubahan state apa pun. PUT, DELETE, dan semua method yang safe bersifat idempotent: mengirim request yang sama dua kali punya efek yang dimaksudkan sama dengan mengirimnya sekali.

POST meminta resource memproses data yang Anda kirim menurut aturannya sendiri, misalnya membuat order yang namanya ditentukan server; jika ada resource yang dibuat, RFC 9110 meminta jawaban 201 Created dengan header Location yang menunjuk ke sana. PUT membuat atau mengganti seluruh state sebuah resource. PATCH, dari RFC 5789, menerapkan sekumpulan perubahan dan tidak safe maupun idempotent, kecuali request-nya Anda rancang begitu. Endpoint kontak situs ini memakai POST karena mengirim pesan adalah pemrosesan, bukan menyimpan resource yang bisa Anda GET kembali.

Jangan pernah menyembunyikan aksi di balik GET. RFC 9110 memakai page?do=delete sebagai contoh dan menyatakan pemilik resource wajib mematikan aksi yang tidak safe jika diakses dengan method yang safe, karena tool otomatis melakukan GET ke setiap link yang ditemukannya untuk mengeceknya, mem-prefetch-nya, atau mengindeksnya.

## Status code HTTP mana yang sebaiknya dikembalikan API?

Status code adalah bagian yang dipahami setiap perangkat lunak HTTP. RFC 9110 mengelompokkan code berdasarkan digit pertamanya: 1xx informational, 2xx sukses, 3xx redirect, 4xx kesalahan client, dan 5xx kesalahan server. Client wajib memahami kelasnya walau tidak mengenal code persisnya, dan memperlakukan 471 yang tidak dikenal sebagai 400. Jadi kelasnya harus jujur: error yang dikirim di dalam 200 terlihat sukses bagi cache, monitor, atau aturan retry mana pun yang hanya membaca status.

Sekumpulan kecil code sudah mencakup sebagian besar API. 200 OK, dan 201 Created dengan Location. 400 Bad Request untuk request yang tidak bisa atau tidak mau diproses server. 401 saat kredensial tidak ada atau tidak valid, yang wajib disertai header WWW-Authenticate, dan 403 saat server memahami request-nya tetapi menolaknya. 404 saat tidak ada apa-apa di URL itu. 405 untuk method yang tidak didukung resource, dengan header Allow. 409 untuk konflik dengan state resource saat ini. 429 Too Many Requests, dari RFC 6585. Dan 5xx hanya saat server gagal memproses request yang tampak valid.

Beberapa pilihan hanyalah soal konvensi, jadi pilih satu dan tuliskan. RFC 9110 mendefinisikan 422 Unprocessable Content untuk isi yang bisa di-parse tetapi tidak bisa diproses; situs ini justru menjawab pemeriksaan field yang gagal dengan 400 dan code invalid_request, dan deskripsi OpenAPI-nya menyebutkan itu. Situs ini juga menjawab path tanpa garis miring di akhir dengan 308 Permanent Redirect ke path yang bergaris miring, jadi client sebaiknya memanggil path persis seperti yang tertulis di kontrak.

## Seperti apa bentuk response error API yang baik?

Satu bentuk untuk setiap kegagalan, supaya client cukup menulis satu parser. Envelope situs ini berisi success yang bernilai false, error untuk manusia, code untuk program, hint tentang cara memperbaikinya, docs yang menunjuk ke /openapi.json, dan retryAfter saat terkena rate limit. Status memberi kategorinya dan code memberi detailnya: tiga jenis 400, yaitu invalid_json, invalid_request, dan rejected_input, butuh tiga reaksi yang berbeda, dan hanya code yang bisa membedakannya.

RFC 9457, Problem Details for HTTP APIs, adalah bentuk standar dari ide yang sama, dikirim sebagai application/problem+json. Anggotanya adalah type, URI yang menjadi identitas utama masalahnya; title, ringkasan singkat jenis masalah itu; status, yang wajib sama dengan status code sebenarnya; detail, penjelasan kejadian ini yang sebaiknya tidak di-parse client; dan instance, URI untuk kejadian ini. Anda boleh menambah extension member, dan client wajib mengabaikan yang tidak dikenalnya. Untuk API publik yang baru, mulailah dari RFC 9457 dan bawa code serta hint sebagai extension.

Perlakukan code sebagai bagian dari kontrak. Tambahkan code baru sesuka Anda, tetapi jangan pernah mengganti nama atau memakai ulang code lama, karena kode client bercabang berdasarkan code itu. Buat pesan error berguna dan bebas dari detail internal: sebut field-nya dan rentang yang diizinkan, jangan pernah stack trace atau error mentah dari provider upstream.

## Bagaimana memberi versi pada API dan memensiunkan endpoint lama?

Sebuah perubahan aman jika semua client yang sudah ada tetap berfungsi, misalnya menambah field opsional atau endpoint baru. Menghapus field, mengganti namanya, mengubah tipenya, atau menjadikan field opsional sebagai wajib akan merusak seseorang. Kebijakan situs ini, yang ditulis sekali di lib/api/versioning.ts, menaruh versi di path, hanya mengizinkan penambahan di dalam satu versi, merilis perubahan yang merusak sebagai /api/v2/, dan menjaga versi yang deprecated tetap berfungsi minimal 180 hari.

Beri tahu client saat runtime, bukan hanya di changelog. Header Deprecation dari RFC 9745 membawa tanggalnya dalam bentuk @ dan detik Unix, bisa di masa lalu atau masa depan, dan RFC itu tegas menyatakan deprecation tidak mengubah apa pun pada perilaku resource. Header Link, yang didefinisikan RFC 8288, menunjuk ke penggantinya dengan rel successor-version dan ke dokumentasi untuk manusia dengan rel deprecation. Begitu tanggal penghapusan ditetapkan, header Sunset dari RFC 8594 menyebutkannya sebagai HTTP date, dan RFC 9745 menyatakan tanggal itu tidak boleh lebih awal dari tanggal Deprecation. Situs ini menjanjikan Sunset paling lambat 90 hari sebelumnya.

GraphQL mengambil jalan lain. Dokumentasinya menjelaskan cara mengembangkan satu schema tanpa repot mengelola banyak versi API: sebuah field ditandai deprecated, lalu dihapus begitu tidak ada lagi yang memakainya.

## Bagaimana rate limit dan pagination melindungi API?

API publik butuh batas per client, kalau tidak satu script bisa membuat semua pengguna lain kehabisan jatah. RFC 6585 mendefinisikan 429 Too Many Requests: response-nya sebaiknya menjelaskan kondisinya, boleh membawa Retry-After yang menyebut berapa lama harus menunggu, dan tidak boleh disimpan cache. RFC itu tidak mengatur cara mengenali client dan menghitung request. Situs ini menghitung per alamat client per menit, 20 untuk chat, 30 untuk follow-up, dan 5 untuk kontak, lalu menjawab dengan Retry-After ditambah X-RateLimit-Limit, X-RateLimit-Remaining, dan X-RateLimit-Reset.

Nama X-RateLimit itu adalah konvensi yang umum, bukan standar. Working group HTTPAPI di IETF punya draft yang mendefinisikan field standar RateLimit dan RateLimit-Policy; per 2026-10-18 draft itu ada di revisi 11 dan belum menjadi RFC, jadi cek statusnya sebelum Anda memakainya.

Setiap endpoint yang mengembalikan daftar butuh pagination sejak rilis pertamanya, karena menambahkannya belakangan mengubah bentuk response. Pagination offset, di mana halaman 3 dengan 20 item per halaman berarti offset 40, memang mudah, tetapi dokumentasi PostgreSQL mengingatkan bahwa baris yang dilewati tetap harus dihitung, jadi offset yang besar itu lambat, dan hasilnya tidak konsisten tanpa ORDER BY yang menghasilkan urutan unik. Pagination cursor, atau keyset, meminta baris setelah baris terakhir yang sudah dilihat, sehingga terhindar dari duplikat yang muncul pada offset saat ada baris baru di antara dua halaman. REST API GitHub mengirim link halaman lewat header Link dengan rel next, prev, first, dan last.

## Kenapa kontraknya dipublikasikan sebagai OpenAPI?

OpenAPI Specification adalah deskripsi API HTTP yang standar dan tidak terikat bahasa pemrograman, yang membuat manusia dan komputer bisa memahami sebuah service tanpa source code-nya. Dari satu dokumen, tool bisa menghasilkan dokumentasi, kode client dan server, serta pengujian. Versi 3.1 mendeskripsikan body request dan response dengan superset dari JSON Schema 2020-12, menyimpan bagian yang dipakai ulang di bawah components, dan menandai sebuah operation sebagai deprecated cukup dengan satu boolean.

Sebuah deskripsi hanya berguna selama isinya benar. Situs ini membangun /openapi.json dari konstanta yang sama dengan yang dipakai route, yaitu path, limit, dan error code, dan sebuah test akan gagal jika endpoint yang didokumentasikan tidak punya file route atau error code yang didokumentasikan berbeda dari daftar aslinya. Entah Anda menulis deskripsinya dulu lalu menghasilkan kode, atau menghasilkan deskripsi dari kode, pegang satu sumber kebenaran dan uji sisi lainnya terhadap sumber itu.

## Konfigurasi nyata di balik pelajaran ini

`lib/api/errors.ts:14-41`

```typescript
/** Stable error codes; also the enum in the OpenAPI `Error` schema. */
export const API_ERROR_CODES = [
  "invalid_json",
  "invalid_request",
  "rejected_input",
  "rate_limited",
  "not_found",
  "method_not_allowed",
  "service_unavailable",
  "upstream_error",
  "internal_error",
] as const;

export type ApiErrorCode = (typeof API_ERROR_CODES)[number];

export interface ApiErrorBody {
  success: false;
  /** Human-readable message, safe to show to a person. */
  error: string;
  /** Stable machine-readable code. */
  code: ApiErrorCode;
  /** What the caller can do about it. */
  hint: string;
  /** The OpenAPI description of this API. */
  docs: string;
  /** Seconds until the caller may retry, for rate limits. */
  retryAfter?: number;
}
```

Envelope dalam bentuk tipe. API_ERROR_CODES adalah satu-satunya daftar code, dan skema Error di OpenAPI menyebarkannya ke enum-nya, jadi kontrak tidak bisa menjanjikan code yang tidak pernah dikembalikan API. ApiErrorBody memisahkan dua pembacanya: error dan hint untuk manusia, code untuk program, docs menunjuk ke kontrak, dan retryAfter hanya muncul saat terkena rate limit.

`lib/api/versioning.ts:83-98`

```typescript
export function deprecationHeaders(
  successor: string,
  deprecatedAt: number = ALIASES_DEPRECATED_AT,
): HttpHeader[] {
  return [
    { key: "Deprecation", value: `@${deprecatedAt}` },
    {
      key: "Link",
      value: [
        `<${BASE_URL}${successor}>; rel="successor-version"`,
        `<${API_POLICY_URL}>; rel="deprecation"; type="text/html"`,
        `<${OPENAPI_URL}>; rel="service-desc"`,
      ].join(", "),
    },
  ];
}
```

Deprecation sebagai data. Nilainya adalah @ diikuti detik Unix, format tanggal RFC 9745; untuk path lama nilainya 1791417600, yaitu tengah malam UTC tanggal 2026-10-08. Nilai Link menggabungkan tiga link RFC 8288: penggantinya (relasi successor-version dari RFC 5829), halaman kebijakan untuk manusia (relasi deprecation dari RFC 9745), dan deskripsi OpenAPI. Komentar dokumentasinya menjelaskan kenapa service-desc diulang: header dari konfigurasi menggantikan Link milik route, bukan menggabungkannya.

`next.config.ts:151-157`

```typescript
      // The unversioned API paths still answer, as deprecated aliases of
      // /api/v1/. Only the slashed form is ever served: the bare one gets
      // the trailingSlash 308 first.
      ...DEPRECATED_API_ALIASES.map(({ path, successor }) => ({
        source: `${path}/`,
        headers: deprecationHeaders(successor),
      })),
```

Tempat header itu dipasang. Setiap alias yang deprecated mendapatkannya dari konfigurasi situs, jadi kode handler tetap sama untuk path lama dan baru, sesuai maksud RFC 9745 bahwa deprecation tidak mengubah perilaku. Source-nya memakai path bergaris miring karena request tanpa garis miring sudah dijawab 308 ke path bergaris miring sebelum bagian ini berjalan.

`lib/api/openapi.ts:416-434`

```typescript
      responses: {
        BadRequest: {
          description:
            "The body is not JSON (`invalid_json`), fails validation (`invalid_request`) or was refused (`rejected_input`).",
          content: { "application/json": { schema: ref("Error") } },
        },
        MethodNotAllowed: {
          description: "Only POST is accepted (`method_not_allowed`).",
          headers: { Allow: header("Allow") },
          content: { "application/json": { schema: ref("Error") } },
        },
        RateLimited: {
          description: "Too many requests from this client (`rate_limited`).",
          headers: {
            "Retry-After": header("RetryAfter"),
            ...RATE_LIMIT_HEADERS,
          },
          content: { "application/json": { schema: ref("Error") } },
        },
```

Response error sebagaimana dinyatakan kontraknya. Masing-masing menyebut code yang bisa dibawanya, 405 mendokumentasikan header Allow-nya, 429 mendokumentasikan Retry-After dan header X-RateLimit, dan semuanya menunjuk ke satu skema Error. tests/agent-readiness.test.mjs gagal jika code di skema itu berbeda dari daftar aslinya, atau jika endpoint yang didokumentasikan tidak punya file route.

## Yang sering rusak

- **Error di dalam 200.** Body-nya bilang error, status-nya bilang OK, jadi cache, monitoring, dan logika retry semuanya menghitungnya sebagai sukses. Kirim status yang sesuai dan simpan detailnya di body. RFC 9457 mewajibkan anggota status sama dengan status code sebenarnya justru karena alasan ini: perangkat lunak HTTP yang generik harus tetap berperilaku benar.
- **Client yang mem-parsing pesan error.** Client yang mencocokkan teks error akan rusak pada hari Anda membetulkan salah ketik di teks itu. Beri setiap kegagalan code yang stabil, dokumentasikan daftarnya, dan anggap penggantian nama code sebagai perubahan yang merusak. RFC 9457 menegaskan hal yang sama: konsumen sebaiknya tidak mem-parsing anggota detail.
- **Perubahan yang merusak di dalam v1.** Mengganti nama field, mengubah tipenya, atau menjadikannya wajib akan merusak client yang tidak mengubah satu baris pun. Di dalam satu versi, hanya tambahkan. Rilis perubahan lainnya sebagai v2 dan biarkan v1 tetap berjalan dengan header Deprecation selama jangka waktu yang Anda janjikan.
- **Garis miring di akhir yang terlupa.** Path di situs ini diakhiri garis miring, dan /api/v1/contact tanpa garis miring dijawab 308 ke /api/v1/contact/. curl tanpa -L, atau client mana pun yang disetel tidak mengikuti redirect, berhenti di 308 dan tidak pernah sampai ke endpoint. Panggil path persis seperti yang tertulis di kontrak.
- **Halaman offset yang dalam pada tabel yang terus berubah.** Offset yang besar membuat database menghitung lalu membuang semua baris sebelumnya, dan baris yang masuk di antara dua request menggeser item antarhalaman, sehingga pembaca melihat duplikat. Pakai cursor untuk daftar yang panjang atau terus bertambah, dan selalu urutkan berdasarkan key yang unik.
- **Yang akan saya ubah di sini: error JSON biasa.** Error di situs ini berupa application/json dengan nama field sendiri, karena chatbot dan form kontak sudah membaca success dan error. Sebuah v2 bisa menjawab dengan application/problem+json dan mempertahankan code serta hint sebagai extension member, sehingga tool yang memahami RFC 9457 bisa membaca error-nya tanpa kode khusus.

### REST, GraphQL, atau gRPC: ditentukan oleh siapa client-nya

|  | REST lewat HTTP | GraphQL | gRPC |
| --- | --- | --- | --- |
| Yang dikirim client | Method pada URL resource, misalnya POST /api/v1/contact/ | Query atau mutation ke satu endpoint, biasanya /graphql | Panggilan ke method yang didefinisikan di service .proto |
| Kontraknya | Deskripsi OpenAPI | Schema beserta sistem tipenya | Definisi protocol buffer, dikompilasi oleh protoc |
| Error | Status HTTP ditambah body error | 2xx dengan daftar errors saat data tidak null; 400 saat query-nya tidak valid | Status code gRPC, misalnya DEADLINE_EXCEEDED |
| Dari browser | Langsung, lewat fetch | POST untuk query dan mutation; GET hanya untuk query | Lewat gRPC-Web dan proxy, Envoy secara default; hanya unary dan server streaming |
| Cara mengubahnya | Versi baru di path, dengan header Deprecation dan Sunset | Kembangkan satu schema; tandai field deprecated, hapus saat tak dipakai | Tambah field; jangan pernah mengubah nomor field yang sudah ada |
| Pilih jika client-nya | Siapa saja: browser, partner, script, dan agent | Beberapa aplikasi yang butuh bentuk berbeda dari data yang sama | Service milik Anda sendiri, di mana Anda memegang kedua sisinya |

## Coba sendiri

Anda butuh curl, dan jq untuk mengambil field dari JSON (hapus pipe-nya untuk melihat body mentahnya). Setiap request di sini hanya membaca atau ditolak sebelum API mengerjakan apa pun: tidak ada yang mengirim pesan.

1. Baca indeks v1. Hasilnya mencetak versi dan tiga endpoint, dan header Link-nya menunjuk ke kontrak (service-desc) serta panduan developer (service-doc).

   ```bash
   curl -s https://www.matthewswong.com/api/v1/ | jq '.version, .endpoints'
   curl -s -D - -o /dev/null https://www.matthewswong.com/api/v1/ | grep -i '^link'
   ```
2. Baca kontraknya: versi OpenAPI, setiap path yang didokumentasikan (path v1 dan alias yang deprecated), serta field yang wajib dibawa request kontak.

   ```bash
   curl -s https://www.matthewswong.com/openapi.json | jq '.openapi, (.paths | keys)'
   curl -s https://www.matthewswong.com/openapi.json | jq '.components.schemas.ContactRequest.required'
   ```
3. Pakai method yang salah: secara default curl mengirim GET. Hasilnya status 405, header allow: POST di antara header response, dan envelope dengan code method_not_allowed.

   ```bash
   curl -si https://www.matthewswong.com/api/v1/contact/
   ```
4. Kirim body yang tidak valid. Objek JSON kosong gagal validasi sebelum apa pun dikirim: hasilnya code invalid_request dan hint yang menyebut field name. Request ini ikut dihitung dalam batas 5 per menit, jadi jangan diulang-ulang.

   ```bash
   curl -s -X POST https://www.matthewswong.com/api/v1/contact/ \
     -H 'Content-Type: application/json' -d '{}' | jq
   ```
5. Minta path yang tidak ada. Anda mendapat 404 dalam JSON dengan code not_found dan hint yang mendaftar endpoint yang benar-benar ada, bukan halaman error HTML.

   ```bash
   curl -s https://www.matthewswong.com/api/v1/orders/ | jq
   ```
6. Panggil path yang deprecated dan baca header-nya: deprecation: @1791417600 dan link dengan rel successor-version yang menunjuk ke /api/v1/contact/. Lalu ubah detik Unix itu menjadi tanggal: 2026-10-08T00:00:00.000Z.

   ```bash
   curl -s -D - -o /dev/null https://www.matthewswong.com/api/contact/ | grep -iE '^(deprecation|link)'
   node -e 'console.log(new Date(1791417600 * 1000).toISOString())'
   ```
7. Baca response rate limit dari kontraknya alih-alih memicunya: 429 mendokumentasikan Retry-After dan tiga header X-RateLimit.

   ```bash
   curl -s https://www.matthewswong.com/openapi.json | jq '.components.responses.RateLimited.headers | keys'
   ```

## Kuis singkat

1. **Sebuah client mengirim PUT /profile, lalu koneksinya putus sebelum ada response. Bolehkah client mengirim PUT yang sama lagi?**
   A. Tidak: PUT mengubah state, jadi mengulanginya tidak pernah aman
   B. Boleh: PUT idempotent, jadi pengulangan punya efek yang dimaksudkan sama
   C. Hanya jika percobaan pertama mengembalikan 201
   Jawaban: B. RFC 9110 menggolongkan PUT sebagai idempotent dan memberi contoh kasus persis ini: client boleh retry karena pengulangannya punya efek yang dimaksudkan sama, bahkan jika percobaan pertama ternyata berhasil. PUT tidak safe, tetapi safe dan idempotent adalah dua sifat yang berbeda.

2. **API Anda menjawab order yang tidak ada dengan status 200 dan pesan error di body. Apa yang salah?**
   A. Cache, monitor, dan client generik menganggapnya sukses
   B. Tidak ada, selama body-nya menyebut error
   C. Browser menolak menampilkan response-nya
   Jawaban: A. Perangkat lunak yang tidak mengenal format body Anda hanya membaca kelas status, dan 2xx berarti sukses. 404 memberi tahu semuanya bahwa request gagal, dan body Anda tetap bisa membawa detail serta code yang stabil.

3. **Anda men-deprecate sebuah endpoint hari ini dan akan menghapusnya enam bulan lagi. Header mana yang memberi tahu client tanggal penghapusannya?**
   A. Deprecation
   B. Retry-After
   C. Sunset
   Jawaban: C. Deprecation menyebut kapan endpoint itu sudah atau akan deprecated dan tidak mengubah perilakunya sama sekali. Sunset, dari RFC 8594, memberi tanggal kapan endpoint itu kemungkinan berhenti merespons, dan tidak boleh lebih awal dari tanggal Deprecation.

## Pikirkan dulu

- **Apa beda method yang safe dengan method yang idempotent?** Method yang safe sama sekali tidak meminta perubahan state: GET, HEAD, OPTIONS, dan TRACE. Method yang idempotent boleh mengubah state, tetapi mengirimnya dua kali punya efek yang dimaksudkan sama dengan sekali: PUT dan DELETE, ditambah semua method yang safe. POST dan PATCH secara default tidak termasuk keduanya.
- **Kenapa body error masih butuh code padahal status-nya sudah 400?** Satu status mencakup beberapa jenis kegagalan. Situs ini mengembalikan 400 untuk invalid_json, invalid_request, dan rejected_input, dan client bereaksi berbeda untuk masing-masing. Code adalah detail stabil yang dipakai program untuk bercabang; error dan hint untuk manusia yang membacanya.
- **Apa yang berubah pada perilaku endpoint setelah header Deprecation ditambahkan?** Tidak ada. RFC 9745 menyatakan deprecation hanya memberi informasi kepada client. Endpoint tetap berfungsi sampai tanggal sunset-nya, sementara header itu dan Link ke penggantinya memberi tahu client bahwa sudah waktunya pindah.
- **Kapan Anda memilih gRPC dibanding REST, dan GraphQL dibanding keduanya?** gRPC saat Anda memegang kedua sisinya, misalnya service internal milik sendiri, dan butuh client yang di-generate serta streaming. GraphQL saat beberapa client butuh bentuk berbeda dari data yang sama. REST saat client-nya bisa siapa saja, mulai dari browser sampai script milik partner yang memakai curl.

## Pelajari lebih dalam

- [REST vs GraphQL: Memilih Desain API yang Tepat](https://www.matthewswong.com/id/blog/api-design-rest-graphql/)
- [Strategi Versioning API: URL, Header, atau Tanpa Versi Sama Sekali](https://www.matthewswong.com/id/blog/api-versioning-strategies-rest/)
- [Cursor vs Offset Pagination: Kesenjangan Performa 17x di Skala](https://www.matthewswong.com/id/blog/cursor-vs-offset-pagination/)
- [gRPC vs REST untuk API Microservices Internal: Perbandingan Praktis](https://www.matthewswong.com/id/blog/microservices-grpc-vs-rest-internal/)

## Sumber

- [RFC 9110: HTTP Semantics (method, status code, Allow, Retry-After)](https://www.rfc-editor.org/rfc/rfc9110.html)
- [RFC 5789: method PATCH untuk HTTP](https://www.rfc-editor.org/rfc/rfc5789.html)
- [RFC 6585: status code HTTP tambahan (429)](https://www.rfc-editor.org/rfc/rfc6585.html)
- [RFC 9457: Problem Details untuk API HTTP](https://www.rfc-editor.org/rfc/rfc9457.html)
- [RFC 9745: header response HTTP Deprecation](https://www.rfc-editor.org/rfc/rfc9745.html)
- [RFC 8594: header HTTP Sunset](https://www.rfc-editor.org/rfc/rfc8594.html)
- [RFC 8288: Web Linking (header Link)](https://www.rfc-editor.org/rfc/rfc8288.html)
- [IANA: registri Link Relations (successor-version, deprecation, service-desc)](https://www.iana.org/assignments/link-relations/link-relations.xhtml)
- [Draft IETF: field header RateLimit untuk HTTP](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/)
- [OpenAPI Specification 3.1.0](https://spec.openapis.org/oas/v3.1.0.html)
- [graphql.org: pengantar GraphQL](https://graphql.org/learn/introduction/)
- [graphql.org: menyajikan GraphQL lewat HTTP](https://graphql.org/learn/serving-over-http/)
- [grpc.io: pengantar gRPC](https://grpc.io/docs/what-is-grpc/introduction/)
- [grpc.io: konsep inti (streaming, deadline)](https://grpc.io/docs/what-is-grpc/core-concepts/)
- [grpc.io: status code dan penggunaannya di gRPC](https://grpc.io/docs/guides/status-codes/)
- [README grpc-web: proxy dan mode RPC yang didukung](https://github.com/grpc/grpc-web)
- [Protocol Buffers: panduan proto3 (mengubah message type)](https://protobuf.dev/programming-guides/proto3/)
- [Dokumentasi PostgreSQL: LIMIT dan OFFSET](https://www.postgresql.org/docs/current/queries-limit.html)
- [Use The Index, Luke: paging tanpa OFFSET](https://use-the-index-luke.com/no-offset)
- [Dokumentasi GitHub: pagination di REST API](https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api)

---

- HTML version: https://www.matthewswong.com/id/learn/system-design/api-design-for-beginners/
- English: https://www.matthewswong.com/en/learn/system-design/api-design-for-beginners/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
