Endpoint REST

Memanggil REST dengan JDK HttpClient, plus pengenalan Micronaut dan Grails.

Daftar isi

REST, singkatan dari Representational State Transfer, adalah gaya penyusunan API HTTP yang menjadikan resource sebagai pusat datanya. Setiap data diakses melalui endpoint, yaitu alamat URL tetap yang berpasangan dengan metode HTTP, dan isi yang dipertukarkan antara klien dan server umumnya berformat JSON. Topik ini membahas cara memanggil endpoint REST dari Groovy tanpa dependensi tambahan, meliputi permintaan (request) GET dan POST dengan klien bawaan JDK HttpClient serta pembacaan kode status dan isi respons (response). Server uji kecilnya dibangun dari paket com.sun.net.httpserver bawaan JDK dan menyalakan endpoint JSON di localhost, sehingga seluruh contoh dapat dijalankan ulang secara offline. Pengenalan dua framework yang lazim dipakai membangun endpoint serupa, yaitu Micronaut dan Grails, melengkapi gambaran ketika endpoint tersebut perlu dibangun sendiri.

Resource, metode HTTP, dan JSON#

Sebuah API REST mengekspos data sebagai resource, yaitu entitas seperti rekap nilai ujian atau catatan sensor suhu yang alamatnya tetap. Perilaku yang diinginkan tidak ditulis di dalam alamatnya, melainkan dinyatakan oleh metode HTTP yang menyertainya. Metode GET membaca resource tanpa mengubahnya, sedangkan POST mengirim data baru atau memicu pemrosesan. Satu endpoint boleh melayani keduanya pada alamat yang sama, misalnya /api/sensor yang melayani GET untuk membaca catatan dan POST untuk menambah pengukuran. Jawaban server membawa kode status tiga digit yang merangkum hasilnya, misalnya 200 untuk pembacaan yang berhasil dan 201 untuk data yang baru dibuat.

Isi yang dipertukarkan berupa teks JSON. Dari sisi Groovy, kelas JsonSlurper dan JsonOutput dari paket groovy.json menangani kedua arah tersebut, yaitu mengurai JSON yang datang bersama respons dan menyusun JSON untuk isi permintaan yang dikirim keluar. HTTP sendiri hanya mengangkut teks biasa, sehingga pola datanya dapat diuji lebih dahulu tanpa jaringan sama sekali.

groovy Contoh: JSON respons diurai dan JSON permintaan disusun
import groovy.json.JsonOutput
import groovy.json.JsonSlurper

// isi JSON yang berpindah lewat HTTP hanyalah teks biasa
def teksRespons = '{"mataKuliah":"Fisika Dasar","jumlahPeserta":32,"rataRata":84.7}'
def rekap = new JsonSlurper().parseText(teksRespons)
println "Diterima : rata-rata ${rekap.rataRata} dari ${rekap.jumlahPeserta} peserta"

def isiKirim = [mataKuliah: 'Kimia Dasar', jumlahPeserta: 28]
println "Dikirim  : ${JsonOutput.toJson(isiKirim)}"
text Keluaran
Diterima : rata-rata 84.7 dari 32 peserta
Dikirim  : {"mataKuliah":"Kimia Dasar","jumlahPeserta":28}

Variabel teksRespons mewakili isi yang nantinya datang dari server, sedangkan baris terakhir menggambarkan isi yang dikirim ke server. Keduanya berupa String, dan kelas HTTP pada bagian berikutnya hanya menambahkan selubung berupa kode status dan header di sekitar teks yang sama.

Server uji bawaan JDK#

Menguji klien terhadap API sungguhan bergantung pada jaringan dan data yang tidak terkendali, sehingga server uji kecil yang disiapkan sendiri di mesin lokal menjadi pilihan yang lebih stabil. JDK membawa server HTTP sederhana pada paket com.sun.net.httpserver, bagian modul jdk.httpserver yang tersedia tanpa unduhan tambahan. Urutan kerjanya tetap, yaitu HttpServer.create mengikat alamat dan port, createContext memasang satu handler untuk satu jalur URL, start menjalankannya, dan stop menghentikannya.

Handler ditulis sebagai closure yang menerima satu objek exchange. Objek tersebut membawa seluruh informasi permintaan sekaligus menjadi tempat menulis jawabannya, yaitu menyetel Content-Type pada responseHeaders, memanggil sendResponseHeaders dengan kode status dan panjang isi, menulis byte jawaban ke responseBody, lalu menutup exchange.

Skrip berikut merangkai kedua sisi dalam satu berkas, yaitu server uji yang melayani GET /api/rekap beserta klien HttpClient yang memanggilnya. Pola uji end-to-end seperti ini membuat demo berjalan penuh di satu mesin tanpa koneksi ke luar, dan keluarannya sama pada setiap eksekusi.

groovy klien-rest.groovy
// klien-rest.groovy: server uji lokal dipanggil JDK HttpClient
import com.sun.net.httpserver.HttpServer
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import java.net.InetSocketAddress
import java.net.BindException
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

// server uji: port 18080, bila terpakai lanjut ke port cadangan tetap
def server = null
def port = 18080
try {
    server = HttpServer.create(new InetSocketAddress(port), 0)
} catch (BindException e) {
    port = 18081
    server = HttpServer.create(new InetSocketAddress(port), 0)
}

server.createContext('/api/rekap') { exchange ->
    def rekap = [mataKuliah: 'Fisika Dasar', peserta: [
        [nama: 'Ali', nilai: 85], [nama: 'Budi', nilai: 78], [nama: 'Citra', nilai: 91]]]
    def isi = JsonOutput.toJson(rekap).getBytes('UTF-8')
    exchange.responseHeaders.set('Content-Type', 'application/json')
    exchange.sendResponseHeaders(200, isi.length)
    exchange.responseBody.write(isi)
    exchange.close()
}

server.start()

def klien = HttpClient.newHttpClient()
def permintaan = HttpRequest.newBuilder()
    .uri(URI.create("http://localhost:${port}/api/rekap"))
    .header('Accept', 'application/json')
    .GET()
    .build()
def respons = klien.send(permintaan, HttpResponse.BodyHandlers.ofString())

println "Status    : ${respons.statusCode()}"
println "Isi       : ${respons.body()}"

def rekap = new JsonSlurper().parseText(respons.body())
println "Nilai     : ${rekap.peserta*.nilai}"
println "Rata-rata : ${rekap.peserta*.nilai.sum() / rekap.peserta.size()}"

server.stop(0)
text Keluaran
Status    : 200
Isi       : {"mataKuliah":"Fisika Dasar","peserta":[{"nama":"Ali","nilai":85},{"nama":"Budi","nilai":78},{"nama":"Citra","nilai":91}]}
Nilai     : [85, 78, 91]
Rata-rata : 84.6666666667

Port 18080 menjadi pilihan utama, dan ketika port itu sedang dipakai proses lain, BindException ditangkap lalu port cadangan 18081 dipakai, sehingga skrip tetap berjalan di mesin yang sibuk. URL permintaan memakai variabel port yang sama dengan milik server, sehingga kedua sisi selalu bertemu pada alamat yang sama. Isi respons diperiksa lewat statusCode dan body, kemudian body diurai JsonSlurper seperti teks JSON biasa, dan perhitungan rata-rata dikerjakan dengan metode koleksi Groovy di atas data hasil permintaan. Baris server.stop(0) menghentikan server dan mengizinkan skrip berakhir, dan melewatkan baris ini justru menjadi kesalahan tersendiri pada bagian Kesalahan umum.

Permintaan POST dengan isi JSON#

Permintaan POST membawa data di dalam isi permintaan (body). Klien Groovy menyusun isinya menjadi String dengan JsonOutput, menyetel header Content-Type ke application/json agar server mengetahui formatnya, lalu mengirimkannya melalui POST dengan HttpRequest.BodyPublishers.ofString. Pada sisi server, isi tersebut dibaca dari requestBody milik exchange, diurai JsonSlurper, dan jawabannya disusun kembali menjadi JSON.

Kode statusnya pun dibedakan dari GET. Angka 200 menyatakan pembacaan yang berhasil, sedangkan 201 Created menandakan resource baru berhasil disimpan. Pembedaan ini membuat klien dapat menilai hasil permintaannya tanpa harus membaca isi respons terlebih dahulu. Header jawaban pada skrip berikut tetap disetel ke application/json, karena isi yang dikembalikan server berformat JSON sekalipun permintaannya POST.

groovy klien-post.groovy
// klien-post.groovy: POST JSON ke server uji dan membaca jawabannya
import com.sun.net.httpserver.HttpServer
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import java.net.InetSocketAddress
import java.net.BindException
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

// server uji: port 18080, bila terpakai lanjut ke port cadangan tetap
def server = null
def port = 18080
try {
    server = HttpServer.create(new InetSocketAddress(port), 0)
} catch (BindException e) {
    port = 18081
    server = HttpServer.create(new InetSocketAddress(port), 0)
}

server.createContext('/api/sensor') { exchange ->
    def catatan = new JsonSlurper().parseText(exchange.requestBody.text)
    def kategori = catatan.suhu >= 28.0 ? 'panas' : 'normal'
    def jawaban = JsonOutput.toJson([status: 'tersimpan', suhu: catatan.suhu, kategori: kategori]).getBytes('UTF-8')
    exchange.responseHeaders.set('Content-Type', 'application/json')
    exchange.sendResponseHeaders(201, jawaban.length)
    exchange.responseBody.write(jawaban)
    exchange.close()
}

server.start()

def catatan = [lokasi: 'ruang peralatan', suhu: 29.4]
def klien = HttpClient.newHttpClient()
def permintaan = HttpRequest.newBuilder()
    .uri(URI.create("http://localhost:${port}/api/sensor"))
    .header('Content-Type', 'application/json')
    .POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(catatan)))
    .build()
def respons = klien.send(permintaan, HttpResponse.BodyHandlers.ofString())

println "Kirim   : ${JsonOutput.toJson(catatan)}"
println "Status  : ${respons.statusCode()}"
println "Respons : ${respons.body()}"

server.stop(0)
text Keluaran
Kirim   : {"lokasi":"ruang peralatan","suhu":29.4}
Status  : 201
Respons : {"status":"tersimpan","suhu":29.4,"kategori":"panas"}

Server membaca seluruh isi permintaan melalui exchange.requestBody.text, lalu menilai kolom suhu untuk menentukan kategorinya. Jawaban yang dikembalikan memuat status penyimpanan dan data yang sudah dikenali, dan klien mencetak kode status 201 beserta isinya. Alur ini memperlihatkan pembagian kerja yang lazim, yaitu klien menyusun JSON dengan JsonOutput, server mengurainya dengan JsonSlurper, dan keduanya hanya bertukar teks.

Micronaut dan Grails#

Menyalakan server dan menulis handler secara manual memperlihatkan mekanismenya, tetapi aplikasi nyata umumnya dititipkan pada framework yang mengurus pemetaan URL, penguraian JSON, dan penanganan error secara seragam. Dua pilihan yang mengenal Groovy dengan baik adalah Micronaut dan Grails, dan keduanya mengerjakan pekerjaan yang sama dengan penekanan yang berbeda.

Micronaut adalah framework aplikasi JVM untuk microservice dan serverless yang menyiapkan dependency injection saat kompilasi, bukan saat runtime. Pendekatan ini memangkas refleksi pada waktu mulai, sehingga aplikasi berjalan cepat dengan konsumsi memori yang kecil. Controller ditulis sebagai kelas beranotasi, dan nilai kembalian berupa map otomatis diserialisasi menjadi JSON, seperti bentuk berikut.

groovy Contoh: controller Micronaut
// tidak-dijalankan
// jar Micronaut tidak tersedia di lingkungan verifikasi
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller('/api/rekap')
class RekapController {

    @Get
    Map rekap() {
        [mataKuliah: 'Fisika Dasar', rataRata: 84.7, jumlahPeserta: 32]
    }
}

Grails menempati posisi yang berbeda, yaitu framework full-stack untuk aplikasi web utuh yang dibangun di atas Spring Boot. Pendekatannya adalah konvensi di atas konfigurasi (convention over configuration), sehingga pemetaan URL, akses basis data melalui GORM, dan tampilan mengikuti penamaan yang telah disepakati kerangkanya. Action pada controller Groovy merender JSON dengan satu pemanggilan render.

groovy Contoh: controller Grails
// tidak-dijalankan
// jar Grails tidak tersedia di lingkungan verifikasi
class RekapController {

    def index() {
        render(contentType: 'application/json') {
            mataKuliah 'Fisika Dasar'
            rataRata 84.7
            jumlahPeserta 32
        }
    }
}

Kedua potongan di atas diberi tanda tidak-dijalankan karena jar framework tidak tersedia di lingkungan verifikasi, sebagaimana jar lain yang lazim diunduh oleh Gradle atau Maven. Bentuk kodenya tetap sah sebagai gambaran, dan pengerjaan nyatanya dimulai dari templat proyek resmi masing-masing framework. Pilihan di antara keduanya mengikuti bentuk aplikasinya, yaitu Micronaut untuk layanan kecil yang cepat berjalan dan Grails untuk aplikasi web lengkap dengan basis data dan tampilannya.

Kesalahan umum#

Error pada pekerjaan endpoint sering bukan berupa kesalahan sintaks, melainkan kondisi port dan jaringan yang dikenali dari exception yang dilempar. Dua berikut ini paling sering ditemui, dan keduanya dapat direproduksi tanpa keadaan mesin yang khusus.

Kesalahan pertama adalah memulai server pada port yang sudah dipakai proses lain. HttpServer.create melempar BindException bila alamat dan portnya sudah dipegang proses lain, dan skrip berhenti pada baris itu. Demo berikut mengunci satu port dengan ServerSocket terlebih dahulu, sehingga percobaan membuat server pada port yang sama pasti gagal tanpa bergantung pada keadaan mesin.

groovy Contoh: port yang sudah dipakai proses lain
// port yang sudah dipegang proses lain tidak dapat dipakai server uji
import com.sun.net.httpserver.HttpServer
import java.net.InetSocketAddress
import java.net.ServerSocket

def pengunci = new ServerSocket(0)
def portTerpakai = pengunci.localPort
try {
    HttpServer.create(new InetSocketAddress(portTerpakai), 0)
} catch (java.net.BindException e) {
    println "Gagal: ${e.getClass().simpleName}"
}
pengunci.close()
text Keluaran
Gagal: BindException

Perbaikannya adalah memilih port lain atau menghentikan proses yang masih memegang port tersebut. Skrip utama topik ini memakai pola serupa dengan menangkap BindException lalu pindah ke port cadangan yang tetap.

Kesalahan kedua adalah memanggil endpoint padahal servernya belum berjalan, misalnya karena start terlewat atau port pada URL-nya salah. Koneksi ke alamat yang tidak mendengarkan gagal dengan ConnectException. Demo berikut memilih port yang masih kosong, menutupnya, lalu mencoba terhubung ke sana.

groovy Contoh: koneksi ketika server belum jalan
// koneksi ke port yang tidak ada servernya gagal dengan ConnectException
import java.net.ServerSocket
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

def soket = new ServerSocket(0)
def portKosong = soket.localPort
soket.close()

def klien = HttpClient.newHttpClient()
def permintaan = HttpRequest.newBuilder()
    .uri(URI.create("http://localhost:${portKosong}/"))
    .GET()
    .build()
try {
    klien.send(permintaan, HttpResponse.BodyHandlers.ofString())
} catch (java.net.ConnectException e) {
    println "Gagal: ${e.getClass().simpleName}"
}
text Keluaran
Gagal: ConnectException

Pesan itu berarti belum ada server yang menerima koneksi pada alamat tersebut. Perbaikannya dilakukan berurutan, yaitu menyalakan server lebih dahulu, memastikan nomor port dan jalur URL klien sama dengan milik server, lalu memanggil ulang permintaannya.

Ringkasan#

  • Endpoint REST memasangkan resource dengan alamat URL dan metode HTTP, sedangkan isi pertukarannya berupa JSON yang diurai JsonSlurper dan disusun JsonOutput.
  • Klien JDK HttpClient dirangkai dari HttpRequest.newBuilder, dikirim dengan send beserta BodyHandlers.ofString, sehingga kode status dan isi respons terbaca langsung.
  • Server uji com.sun.net.httpserver menyalakan endpoint JSON lokal melalui create, createContext, dan start, lalu wajib diakhiri stop(0) agar skrip berakhir.
  • Micronaut menyiapkan dependency injection saat kompilasi untuk layanan yang ringan, sedangkan Grails adalah framework full-stack Groovy di atas Spring Boot, dan keduanya menggantikan penulisan server manual dengan controller.

Referensi#