BAB 48: Web Service API — Menyajikan Data sebagai JSON
Bangun REST API di Go menggunakan net/http dan encoding/json — dari endpoint sederhana, query parameter, hingga penanganan HTTP method yang benar.
Di bab sebelumnya program bisa mengonversi struct Artikel ke JSON dan sebaliknya. Kemampuan ini berguna untuk menyimpan atau memproses data, tapi masih terbatas — datanya diam di dalam program. Bagaimana kalau program ingin menyajikan data itu ke dunia luar? Bagaimana kalau ada aplikasi lain, atau browser, yang perlu mengambil daftar artikel melalui HTTP?
Di sinilah kedua bab bertemu: web server dari Bab 45 dan JSON dari Bab 47. Menggabungkan keduanya menghasilkan web service API — program yang menerima HTTP request dan membalas dengan data JSON. Ini adalah pola yang menjadi tulang punggung hampir semua backend modern.
Struktur Dasar Web Service
Web service API yang paling sederhana punya tiga bagian: data yang ingin disajikan, handler yang memproses request, dan server yang mendengarkan koneksi masuk.
Mulai dengan contoh paling minimal — endpoint yang mengembalikan daftar artikel:
// api-artikel.go
package main
import (
"encoding/json"
"net/http"
)
type Artikel struct {
ID int `json:"id"`
Judul string `json:"judul"`
Penulis string `json:"penulis"`
Topik string `json:"topik"`
}
var daftarArtikel = []Artikel{
{ID: 1, Judul: "Goroutine dan Concurrency", Penulis: "Budi Santoso", Topik: "concurrency"},
{ID: 2, Judul: "Memahami Interface di Go", Penulis: "Rina Wijaya", Topik: "oop"},
{ID: 3, Judul: "Bekerja dengan Channel", Penulis: "Hendra Kusuma", Topik: "concurrency"},
}
func handlerArtikel(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(daftarArtikel)
}
func main() {
http.HandleFunc("/api/artikel", handlerArtikel)
http.ListenAndServe(":8080", nil)
}
Jalankan program, lalu akses dari terminal:
curl http://localhost:8080/api/artikel
Output:
[
{"id":1,"judul":"Goroutine dan Concurrency","penulis":"Budi Santoso","topik":"concurrency"},
{"id":2,"judul":"Memahami Interface di Go","penulis":"Rina Wijaya","topik":"oop"},
{"id":3,"judul":"Bekerja dengan Channel","penulis":"Hendra Kusuma","topik":"concurrency"}
]
Ada dua hal baru di sini. Pertama, w.Header().Set("Content-Type", "application/json") memberitahu klien bahwa respons berformat JSON — ini penting agar browser dan HTTP client bisa memproses respons dengan benar. Kedua, json.NewEncoder(w).Encode() menulis JSON langsung ke ResponseWriter tanpa membuat buffer sementara — lebih efisien daripada json.Marshal() untuk respons HTTP.
Filter dengan Query Parameter
Endpoint yang hanya mengembalikan semua data jarang cukup di dunia nyata. Klien sering perlu mengambil satu artikel berdasarkan ID, atau memfilter berdasarkan kriteria tertentu. Query parameter adalah cara standar untuk menyampaikan filter ini.
// api-artikel-filter.go
package main
import (
"encoding/json"
"net/http"
"strconv"
)
type Artikel struct {
ID int `json:"id"`
Judul string `json:"judul"`
Penulis string `json:"penulis"`
Topik string `json:"topik"`
}
var daftarArtikel = []Artikel{
{ID: 1, Judul: "Goroutine dan Concurrency", Penulis: "Budi Santoso", Topik: "concurrency"},
{ID: 2, Judul: "Memahami Interface di Go", Penulis: "Rina Wijaya", Topik: "oop"},
{ID: 3, Judul: "Bekerja dengan Channel", Penulis: "Hendra Kusuma", Topik: "concurrency"},
}
func handlerArtikel(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
idStr := r.FormValue("id")
if idStr == "" {
// tidak ada filter — kembalikan semua
json.NewEncoder(w).Encode(daftarArtikel)
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, `{"error":"id tidak valid"}`, http.StatusBadRequest)
return
}
for _, a := range daftarArtikel {
if a.ID == id {
json.NewEncoder(w).Encode(a)
return
}
}
http.Error(w, `{"error":"artikel tidak ditemukan"}`, http.StatusNotFound)
}
func main() {
http.HandleFunc("/api/artikel", handlerArtikel)
http.ListenAndServe(":8080", nil)
}
Dengan kode ini, endpoint /api/artikel mendukung dua pola akses sekaligus:
# ambil semua artikel
curl http://localhost:8080/api/artikel
# ambil artikel dengan id tertentu
curl http://localhost:8080/api/artikel?id=2
# id tidak ada
curl http://localhost:8080/api/artikel?id=99
Output untuk ?id=2:
{"id":2,"judul":"Memahami Interface di Go","penulis":"Rina Wijaya","topik":"oop"}
Output untuk ?id=99:
{"error":"artikel tidak ditemukan"}
r.FormValue() adalah cara yang sudah dikenal dari Bab 45 — fungsi ini membaca query parameter dari URL. Perbedaan kali ini ada di respons: bukan HTML, melainkan JSON yang terstruktur.
http.Error() menerima string pesan dan status code. Untuk API yang mengembalikan JSON, pastikan pesan error juga berbentuk JSON agar klien bisa mem-parsing respons error dengan cara yang sama seperti respons sukses.
Menangani HTTP Method
Sampai titik ini handler menerima request apapun tanpa peduli method-nya — GET, POST, DELETE semuanya direspons sama. Ini bermasalah karena API yang baik menggunakan method HTTP sesuai semantiknya: GET untuk membaca data, POST untuk membuat data baru.
Go tidak punya router bawaan yang memetakan method, tapi r.Method memberikan method request yang sedang masuk:
// api-artikel-method.go
package main
import (
"encoding/json"
"net/http"
"strconv"
)
type Artikel struct {
ID int `json:"id"`
Judul string `json:"judul"`
Penulis string `json:"penulis"`
Topik string `json:"topik"`
}
var daftarArtikel = []Artikel{
{ID: 1, Judul: "Goroutine dan Concurrency", Penulis: "Budi Santoso", Topik: "concurrency"},
{ID: 2, Judul: "Memahami Interface di Go", Penulis: "Rina Wijaya", Topik: "oop"},
{ID: 3, Judul: "Bekerja dengan Channel", Penulis: "Hendra Kusuma", Topik: "concurrency"},
}
func handlerArtikel(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
idStr := r.FormValue("id")
if idStr == "" {
json.NewEncoder(w).Encode(daftarArtikel)
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, `{"error":"id tidak valid"}`, http.StatusBadRequest)
return
}
for _, a := range daftarArtikel {
if a.ID == id {
json.NewEncoder(w).Encode(a)
return
}
}
http.Error(w, `{"error":"artikel tidak ditemukan"}`, http.StatusNotFound)
default:
http.Error(w, `{"error":"method tidak didukung"}`, http.StatusMethodNotAllowed)
}
}
func main() {
http.HandleFunc("/api/artikel", handlerArtikel)
http.ListenAndServe(":8080", nil)
}
Mencoba mengirim request dengan method yang salah sekarang menghasilkan respons yang bermakna:
curl -X POST http://localhost:8080/api/artikel
Output:
{"error":"method tidak didukung"}
Status code 405 Method Not Allowed juga dikirim bersama respons — klien HTTP yang baik akan membaca status code ini, bukan hanya isi body-nya.
Respons yang Konsisten
API yang matang selalu membungkus data dalam struktur respons yang konsisten — bukan hanya membuang data mentah. Ini memudahkan klien karena selalu tahu di mana mencari data dan di mana mencari pesan error.
// api-kontenku.go
package main
import (
"encoding/json"
"net/http"
"strconv"
)
type Artikel struct {
ID int `json:"id"`
Judul string `json:"judul"`
Penulis string `json:"penulis"`
Topik string `json:"topik"`
}
type Respons struct {
Sukses bool `json:"sukses"`
Pesan string `json:"pesan"`
Data interface{} `json:"data,omitempty"`
}
var daftarArtikel = []Artikel{
{ID: 1, Judul: "Goroutine dan Concurrency", Penulis: "Budi Santoso", Topik: "concurrency"},
{ID: 2, Judul: "Memahami Interface di Go", Penulis: "Rina Wijaya", Topik: "oop"},
{ID: 3, Judul: "Bekerja dengan Channel", Penulis: "Hendra Kusuma", Topik: "concurrency"},
}
func kirimJSON(w http.ResponseWriter, status int, payload Respons) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(payload)
}
func handlerArtikel(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
kirimJSON(w, http.StatusMethodNotAllowed, Respons{
Sukses: false,
Pesan: "hanya GET yang didukung",
})
return
}
idStr := r.FormValue("id")
if idStr == "" {
kirimJSON(w, http.StatusOK, Respons{
Sukses: true,
Pesan: "berhasil mengambil semua artikel",
Data: daftarArtikel,
})
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
kirimJSON(w, http.StatusBadRequest, Respons{
Sukses: false,
Pesan: "format id tidak valid",
})
return
}
for _, a := range daftarArtikel {
if a.ID == id {
kirimJSON(w, http.StatusOK, Respons{
Sukses: true,
Pesan: "artikel ditemukan",
Data: a,
})
return
}
}
kirimJSON(w, http.StatusNotFound, Respons{
Sukses: false,
Pesan: "artikel tidak ditemukan",
})
}
func main() {
http.HandleFunc("/api/artikel", handlerArtikel)
http.ListenAndServe(":8080", nil)
}
Sekarang semua respons — sukses maupun error — mengikuti format yang sama:
curl http://localhost:8080/api/artikel
{"sukses":true,"pesan":"berhasil mengambil semua artikel","data":[...]}
curl http://localhost:8080/api/artikel?id=99
{"sukses":false,"pesan":"artikel tidak ditemukan"}
Fungsi kirimJSON() menghilangkan duplikasi kode header dan encoding yang sebelumnya harus ditulis ulang di setiap titik respons. Ini juga tempat yang tepat untuk menambahkan logging atau header tambahan nanti.
w.WriteHeader() harus dipanggil sebelum menulis body. Setelah body mulai ditulis, status code tidak bisa diubah lagi. Di kirimJSON(), urutan ini sudah dijaga: w.Header().Set() dulu, lalu w.WriteHeader(), lalu json.NewEncoder(w).Encode().
Menguji API dengan curl
curl adalah tool paling cepat untuk menguji endpoint tanpa perlu aplikasi tambahan. Beberapa flag yang berguna:
# GET biasa
curl http://localhost:8080/api/artikel
# GET dengan query parameter
curl "http://localhost:8080/api/artikel?id=1"
# Tampilkan header respons
curl -i http://localhost:8080/api/artikel
# Tentukan method secara eksplisit
curl -X GET http://localhost:8080/api/artikel
# POST ke endpoint yang hanya menerima GET
curl -X POST http://localhost:8080/api/artikel
Untuk melihat respons yang lebih rapi, curl bisa digabung dengan python3 -m json.tool:
curl -s http://localhost:8080/api/artikel | python3 -m json.tool
Ini akan mencetak JSON dengan indentasi yang rapi — setara dengan json.MarshalIndent() yang sudah dipelajari di Bab 47.
Latihan
Latihan 1 — Endpoint filter topik:
Tambahkan query parameter topik ke endpoint /api/artikel. Ketika ?topik=concurrency dikirim, kembalikan hanya artikel yang topiknya cocok. Ketika ?topik= kosong, kembalikan semua artikel seperti biasa.
Latihan 2 — Multiple endpoint:
Tambahkan endpoint /api/penulis yang mengembalikan daftar penulis unik dari daftarArtikel. Gunakan map untuk menghilangkan duplikat, lalu konversi ke slice sebelum mengirimnya sebagai JSON.
Latihan 3 — Respons dengan metadata:
Ubah endpoint /api/artikel agar respons untuk semua artikel menyertakan field total di dalam data:
{"sukses":true,"pesan":"...","data":{"total":3,"artikel":[...]}}
Hint: buat struct baru DataArtikelList dengan field Total int dan Artikel []Artikel, lalu gunakan struct itu sebagai Data di struct Respons.
Program Go kini tidak hanya bisa membaca, menulis, dan memformat JSON — tapi juga menyajikannya melalui HTTP ke dunia luar. Pola handler + struct respons + kirimJSON() yang dibentuk di bab ini adalah fondasi yang sama dipakai di proyek Go nyata, terlepas dari seberapa kompleks API-nya nanti. Langkah berikutnya adalah membalik arahnya: bukan hanya menjadi server yang melayani request, tapi juga menjadi klien yang mengonsumsi API dari layanan eksternal.