Sebelum menulis kode, kita perlu menetapkan kontrak API: endpoint apa yang disediakan, request body seperti apa, response apa yang dikembalikan, dan status code mana yang dipakai untuk setiap kasus. Pada Bagian 1 kita merancang kontrak ini sebagai dokumen yang dapat menjadi rujukan implementasi di Bagian 2 hingga 4.

Kontrak REST API #

Kontrak API terdiri dari lima elemen:

  • HTTP method — GET, POST, PUT, DELETE.
  • Path — jalur URL beserta parameter.
  • Request body — payload JSON yang dikirim klien.
  • Response body — payload JSON yang dikembalikan server.
  • Status code — kode status HTTP yang menunjukkan hasil.

Lima elemen ini harus konsisten agar klien dapat memprediksi perilaku API. Dokumentasi OpenAPI/Swagger sering dipakai untuk menstandarkan kontrak, tetapi untuk belajar kita memakai deklarasi Kotlin yang dapat dieksekusi sebagai living documentation.

Deklarasi Endpoint #

Kode berikut mendeklarasikan endpoint sebagai data class dan enum:

kotlin
// Bagian 1: Desain REST API.
// Mendemonstrasikan struktur endpoint, request/response, dan status code
// untuk manajemen tugas, tanpa implementasi server nyata.

data class Tugas(
    val id: Int,
    val judul: String,
    val selesai: Boolean = false
)

data class PermintaanBuat(val judul: String)
data class PermintaanPerbarui(val judul: String? = null, val selesai: Boolean? = null)
data class Respons<T>(val data: T? = null, val galat: String? = null, val status: Int)

enum class HttpStatus(val kode: Int, val label: String) {
    OK(200, "OK"),
    CREATED(201, "Created"),
    NO_CONTENT(204, "No Content"),
    BAD_REQUEST(400, "Bad Request"),
    NOT_FOUND(404, "Not Found"),
    CONFLICT(409, "Conflict"),
    UNAUTHORIZED(401, "Unauthorized"),
    INTERNAL_ERROR(500, "Internal Server Error")
}

object SkemaEndpoint {
    data class Endpoint(
        val metode: String,
        val jalur: String,
        val deskripsi: String,
        val statusBerhasil: List<Int>,
        val statusGagal: List<Int>
    )

    val daftar = listOf(
        Endpoint("GET",    "/tasks",      "Daftar semua tugas", listOf(200), listOf(401, 500)),
        Endpoint("POST",   "/tasks",      "Buat tugas baru", listOf(201), listOf(400, 401, 500)),
        Endpoint("GET",    "/tasks/{id}", "Ambil tugas by id", listOf(200), listOf(404, 401, 500)),
        Endpoint("PUT",    "/tasks/{id}", "Perbarui tugas by id", listOf(200), listOf(400, 404, 401, 500)),
        Endpoint("DELETE", "/tasks/{id}", "Hapus tugas by id", listOf(204), listOf(404, 401, 500)),
        Endpoint("POST",   "/auth/login", "Login dan dapatkan JWT", listOf(200), listOf(401, 500))
    )
}

fun main() {
    println("=== Endpoint REST API ===")
    println("%-7s %-18s %-25s %s".format("Metode", "Jalur", "Deskripsi", "Status Berhasil"))
    SkemaEndpoint.daftar.forEach { e ->
        println("%-7s %-18s %-25s %s".format(
            e.metode, e.jalur, e.deskripsi, e.statusBerhasil.joinToString(",")
        ))
    }

    println()
    println("=== Skema request/response ===")
    val contohBuat = PermintaanBuat("Belajar Ktor")
    println("POST /tasks body: ${contohBuat}")
    val respons = Respons(data = Tugas(1, contohBuat.judul), status = HttpStatus.CREATED.kode)
    println("Response 201: ${respons}")

    println()
    println("=== Status code umum ===")
    HttpStatus.values().forEach { println("${it.kode} ${it.label}") }

    println()
    println("=== Skenario galat ===")
    val gagalValidasi = Respons<Tugas>(galat = "Judul tidak boleh kosong", status = HttpStatus.BAD_REQUEST.kode)
    println("400: ${gagalValidasi.galat}")
    val tidakAda = Respons<Tugas>(galat = "Tugas id=99 tidak ditemukan", status = HttpStatus.NOT_FOUND.kode)
    println("404: ${tidakAda.galat}")
}

Output yang Dihasilkan:

text
=== Endpoint REST API ===
Metode  Jalur              Deskripsi                 Status Berhasil
GET     /tasks             Daftar semua tugas        200
POST    /tasks             Buat tugas baru           201
GET     /tasks/{id}        Ambil tugas by id         200
PUT     /tasks/{id}        Perbarui tugas by id      200
DELETE  /tasks/{id}        Hapus tugas by id         204
POST    /auth/login        Login dan dapatkan JWT    200

=== Skema request/response ===
POST /tasks body: PermintaanBuat(judul=Belajar Ktor)
Response 201: Respons(data=Tugas(id=1, judul=Belajar Ktor, selesai=false), galat=null, status=201)

=== Status code umum ===
200 OK
201 Created
204 No Content
400 Bad Request
404 Not Found
409 Conflict
401 Unauthorized
500 Internal Server Error

=== Skenario galat ===
400: Judul tidak boleh kosong
404: Tugas id=99 tidak ditemukan

Mari kita bedah elemen desain:

  1. data class PermintaanBuat(val judul: String): request body untuk POST /tasks. Hanya satu field yang wajib: judul.
  2. data class PermintaanPerbarui(val judul: String? = null, val selesai: Boolean? = null): request body untuk PUT /tasks/{id}. Semua field nullable dengan default null, sehingga klien dapat memperbarui sebagian (partial update).
  3. data class Respons<T>(val data: T?, val galat: String?, val status: Int): respons generik. data berisi payload sukses, galat berisi pesan galat, status menunjukkan kode HTTP. Hanya satu dari data/galat yang terisi dalam praktik.
  4. enum class HttpStatus(val kode: Int, val label: String): setiap konstanta membawa dua property. Akses HttpStatus.CREATED.kode mengembalikan 201.
  5. object SkemaEndpoint: tunggal, menyimpan daftar endpoint. Property daftar berupa List<Endpoint> yang immutable.
  6. "GET" to "/tasks": bentuk Pair<String, String> untuk memetakan metode dan jalur. Dipakai di Bagian 2 untuk routing.

Pemilihan Status Code #

Status code HTTP dibagi lima rentang:

RentangArtiContoh
1xxInformationalJarang dipakai
2xxSukses200 OK, 201 Created, 204 No Content
3xxRedirection301 Moved, 304 Not Modified
4xxGalat klien400 Bad Request, 404 Not Found, 401 Unauthorized
5xxGalat server500 Internal Error, 503 Service Unavailable

Aturan pemilihan untuk proyek ini:

  • POST sukses → 201 Created (sumber daya baru dibuat).
  • GET, PUT sukses → 200 OK.
  • DELETE sukses → 204 No Content (tidak ada body).
  • Validasi gagal → 400 Bad Request.
  • Sumber daya tidak ditemukan → 404 Not Found.
  • Tidak terotentikasi → 401 Unauthorized.

Idempotency #

Empat method HTTP memiliki sifat idempotensi yang berbeda:

  • GET — idempotent dan safe (tidak mengubah state).
  • PUT — idempotent (memanggil dua kali memberi hasil yang sama).
  • DELETE — idempotent (menghapus dua kali: pertama hapus, kedua tidak ada efek).
  • POST — non-idempotent (memanggil dua kali membuat dua sumber daya).

Memahami sifat ini penting untuk retry otomatis dan caching. Klien dapat mengulang PUT atau DELETE tanpa takut duplikat, tetapi harus berhati-hati dengan POST.

Referensi Lanjutan #

  • Ktor routing — panduan routing Ktor untuk merancang endpoint REST manajemen tugas

Ringkasan Bagian 1 #

Kontrak API kini terdefinisi: enam endpoint dengan method, path, dan status code yang konsisten. Request dan response body dideklarasikan sebagai data class, dan HttpStatus menjadi sumber tunggal kebenaran untuk kode status. Pada Bagian 2 kita mengimplementasikan routing yang melayani endpoint ini.