Status code adalah tata bahasa kesepakatan HTTP: angka pertama menyatakan kelas makna, dan klien yang menghormati kontrak ini bisa mengambil keputusan otomatis tanpa mem-parsing body. API yang menjawab 200 untuk segala hal lalu menyisipkan pesan gagal di dalam JSON memaksa klien menebak dari isi body — pendekatan rapuh yang mudah salah. Pada chapter ini kita memetakan status code yang relevan untuk REST API, membahas pilihan yang sering diperdebatkan (400 lawan 422, kapan 409 dipakai), lalu membuktikan penerapannya lewat program yang menunjukkan 201, 409, 400, 404, dan 200 dalam lima permintaan.
Tabel Status Code untuk REST API #
| Status | Nama | Konteks pemakaian pada API |
|---|---|---|
| 200 | OK | GET dan PUT sukses dengan body; DELETE sukses dengan body ringkas |
| 201 | Created | POST berhasil membuat resource; sertakan header Location |
| 204 | No Content | sukses tanpa body — khas DELETE dan PUT yang tidak mengembalikan representasi |
| 400 | Bad Request | payload rusak, JSON tidak sah, atau parameter tidak valid |
| 401 | Unauthorized | kredensial tidak ada atau salah — minta autentikasi ulang |
| 403 | Forbidden | sudah terautentikasi, tetapi tidak berhak mengakses resource |
| 404 | Not Found | resource tidak ada, atau path tidak dikenal |
| 409 | Conflict | konflik dengan state saat ini, misalnya duplikat data unik |
| 422 | Unprocessable Entity | JSON sah secara sintaks tetapi gagal aturan bisnis (konvensi tim) |
| 429 | Too Many Requests | batas laju permintaan terlampaui |
| 500 | Internal Server Error | kegagalan tak terduga di sisi server |
| 503 | Service Unavailable | server sementara tidak melayani — maintenance atau kelebihan beban |
Dua baris pertama tabel menjadi aturan baku pembuatan resource: 201 untuk POST, disertai Location yang menunjuk resource baru. Baris 401 dan 403 membedakan “belum diverifikasi” dari “terverifikasi tapi ditolak” — pencampuran keduanya membuat klien salah menawarkan layar login pada pengguna yang sebenarnya kekurangan izin.
400 atau 422 #
Kedua status sama-sama menolak permintaan klien, tetapi letak kesalahannya berbeda. Status 400 dipakai ketika permintaan salah bentuk: JSON rusak, tipe field tidak cocok, parameter tidak lengkap. Status 422 dipakai ketika bentuk permintaan benar tetapi maknanya ditolak aturan bisnis — email valid secara format namun sudah terpakai, umur 15 untuk pendaftaran dewasa. Standard library dan chapter sebelumnya memakai 400 untuk kedua kondisi; pilihan itu sah dan umum. Bila tim memilih 422 untuk kegagalan aturan bisnis, gunakan secara konsisten di seluruh endpoint — konsistensi jauh lebih penting daripada pilihan itu sendiri.
409 untuk Konflik Duplikat #
Dua permintaan POST dengan email yang sama pada endpoint /users menghasilkan konflik terhadap keunikan data. Status 409 mengomunikasikan kondisi tersebut: permintaan individual valid, resource saat inilah yang membuat operasi tidak bisa dilanjutkan. Body galat sebaiknya menyebut kolom yang berkonflik supaya klien langsung tahu field mana yang diganti.
Body Galat yang Konsisten #
Setiap respons gagal pada chapter ini memakai kerangka sama: {"error":{"code":"...","message":"..."}}. Field code berisi konstanta mesin yang stabil untuk dirujuk program klien; field message berisi kalimat yang bisa langsung ditampilkan manusia. Klien cukup bercabang pada nilai code untuk pengambilan keputusan dan tidak pernah mem-parsing message.
Program: 201, 409, 400, 404, dan 200 #
Program status-demo.go melayani pembuatan pengguna dengan deteksi email duplikat dan pembacaan pengguna berdasarkan ID. Lima permintaan menampilkan lima status berbeda beserta body-nya.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"time"
)
// Pengguna adalah representasi resource yang dikirim ke klien.
type Pengguna struct {
ID int `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
// masukanPengguna adalah payload pembuatan pengguna.
type masukanPengguna struct {
Email string `json:"email"`
Name *string `json:"name"`
}
// Handler menyimpan pengguna beserta indeks email untuk deteksi duplikat.
type Handler struct {
data []Pengguna
emailAda map[string]bool
waktuDib time.Time
}
func (h *Handler) routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("POST /users", h.buat)
mux.HandleFunc("GET /users/{id}", h.detail)
return mux
}
func (h *Handler) buat(w http.ResponseWriter, r *http.Request) {
var in masukanPengguna
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
tulisGalat(w, http.StatusBadRequest, "INVALID_JSON", "body bukan JSON yang valid")
return
}
if in.Name == nil || strings.TrimSpace(*in.Name) == "" {
tulisGalat(w, http.StatusBadRequest, "VALIDATION_ERROR", "name wajib diisi")
return
}
if h.emailAda[in.Email] {
tulisGalat(w, http.StatusConflict, "EMAIL_TELAH_DIPAKAI",
fmt.Sprintf("email %s sudah terdaftar", in.Email))
return
}
u := Pengguna{
ID: len(h.data) + 1,
Email: in.Email,
Name: strings.TrimSpace(*in.Name),
CreatedAt: h.waktuDib,
}
h.data = append(h.data, u)
h.emailAda[in.Email] = true
w.Header().Set("Location", fmt.Sprintf("/users/%d", u.ID))
tulisJSON(w, http.StatusCreated, u)
}
func (h *Handler) detail(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
tulisGalat(w, http.StatusBadRequest, "INVALID_ID", "id harus angka")
return
}
for _, u := range h.data {
if u.ID == id {
tulisJSON(w, http.StatusOK, u)
return
}
}
tulisGalat(w, http.StatusNotFound, "NOT_FOUND",
fmt.Sprintf("pengguna dengan id %d tidak ditemukan", id))
}
type galatBody struct {
Error galatRincian `json:"error"`
}
type galatRincian struct {
Code string `json:"code"`
Message string `json:"message"`
}
func tulisGalat(w http.ResponseWriter, status int, kode, pesan string) {
tulisJSON(w, status, galatBody{Error: galatRincian{Code: kode, Message: pesan}})
}
func tulisJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func main() {
h := &Handler{
emailAda: make(map[string]bool),
waktuDib: time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC),
}
mux := h.routes()
// 201: sumber daya baru berhasil dibuat, Location menunjuk padanya.
kirim(mux, "POST", "/users", `{"email":"[email protected]","name":"Budi"}`)
// 409: email sudah terdaftar — konflik duplikat.
kirim(mux, "POST", "/users", `{"email":"[email protected]","name":"Budi Kedua"}`)
// 400: payload JSON sah tetapi field name hilang.
kirim(mux, "POST", "/users", `{"email":"[email protected]"}`)
// 200: pembacaan berhasil.
kirim(mux, "GET", "/users/1", "")
// 404: sumber daya tidak ada.
kirim(mux, "GET", "/users/99", "")
}
// kirim mengeksekusi satu permintaan lalu mencetak dump status, Location, dan body.
func kirim(mux *http.ServeMux, metode, target, body string) {
var pembaca io.Reader
if body != "" {
pembaca = strings.NewReader(body)
}
req := httptest.NewRequest(metode, target, pembaca)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
fmt.Printf("%-5s %-11s -> %d", metode, target, rec.Code)
if loc := rec.Header().Get("Location"); loc != "" {
fmt.Printf(" (Location: %s)", loc)
}
fmt.Println()
if rec.Body.Len() > 0 {
fmt.Printf(" %s", rec.Body.String())
}
}Output yang Dihasilkan:
POST /users -> 201 (Location: /users/1)
{"id":1,"email":"[email protected]","name":"Budi","created_at":"2026-01-15T08:00:00Z"}
POST /users -> 409
{"error":{"code":"EMAIL_TELAH_DIPAKAI","message":"email [email protected] sudah terdaftar"}}
POST /users -> 400
{"error":{"code":"VALIDATION_ERROR","message":"name wajib diisi"}}
GET /users/1 -> 200
{"id":1,"email":"[email protected]","name":"Budi","created_at":"2026-01-15T08:00:00Z"}
GET /users/99 -> 404
{"error":{"code":"NOT_FOUND","message":"pengguna dengan id 99 tidak ditemukan"}}Mari kita telaah baris demi baris program di atas:
- Baris 28-33 (
type Handler): selain slice data, handler menyimpan petaemailAdasebagai indeks keunikan email untuk mendeteksi duplikat secara cepat. - Baris 42-47 (cabang 400): decode yang gagal dijawab 400 dengan kode
INVALID_JSON— permintaan salah bentuk. - Baris 48-51 (cabang 400 kedua): field
nameyang hilang dijawab 400VALIDATION_ERROR; masuk kategori masukan tidak valid, bukan konflik. - Baris 52-56 (cabang 409): email yang sudah terdaftar dijawab 409
EMAIL_TELAH_DIPAKAIdengan pesan yang menyebut email penyebab konflik. - Baris 64-67 (sukses 201): resource disimpan, peta email diperbarui, header
Locationdiset, lalu status 201 dan representasi pengguna dikirim. - Baris 70-84 (
detail): loop pencarian menjawab 200 dengan data saat ID ditemukan, dan 404NOT_FOUNDsaat tidak. - Baris 86-97 (
tulisGalat): satu fungsi menjamin seluruh respons gagal berbagi kerangka{"error":{"code","message"}}yang sama. - Baris 105-126 (
main): lima skenario berurutan dari sukses ke galat agar tiap baris output mudah dibandingkan dengan tabel status.
Lima baris permintaan pada output memetakan lima skenario: 201 dengan Location, 409 untuk duplikat, 400 untuk field hilang, 200 untuk pembacaan, dan 404 untuk resource yang tidak ada. Perhatikan bahwa body keempat respons galat memakai kerangka identik — hanya code dan message yang berbeda.
Status code menjawab “apa yang terjadi” secara ringkas. Ketika data yang dikembalikan jumlahnya ratusan baris, pertanyaan berikutnya adalah “bagaimana memotongnya” — jawabannya, pagination, filter, dan sorting, menjadi chapter berikutnya.
Referensi #
- Dokumentasi package net/http
— konstanta status dan perilaku
ResponseWriter. - Dokumentasi package httptest — pengujian status dan body handler tanpa jaringan.
- Dokumentasi package fmt — pemformatan pesan galat yang informatif.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
status-demo.go
Unduh berkaspackage main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"time"
)
// Pengguna adalah representasi resource yang dikirim ke klien.
type Pengguna struct {
ID int `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
// masukanPengguna adalah payload pembuatan pengguna.
type masukanPengguna struct {
Email string `json:"email"`
Name *string `json:"name"`
}
// Handler menyimpan pengguna beserta indeks email untuk deteksi duplikat.
type Handler struct {
data []Pengguna
emailAda map[string]bool
waktuDib time.Time
}
func (h *Handler) routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("POST /users", h.buat)
mux.HandleFunc("GET /users/{id}", h.detail)
return mux
}
func (h *Handler) buat(w http.ResponseWriter, r *http.Request) {
var in masukanPengguna
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
tulisGalat(w, http.StatusBadRequest, "INVALID_JSON", "body bukan JSON yang valid")
return
}
if in.Name == nil || strings.TrimSpace(*in.Name) == "" {
tulisGalat(w, http.StatusBadRequest, "VALIDATION_ERROR", "name wajib diisi")
return
}
if h.emailAda[in.Email] {
tulisGalat(w, http.StatusConflict, "EMAIL_TELAH_DIPAKAI",
fmt.Sprintf("email %s sudah terdaftar", in.Email))
return
}
u := Pengguna{
ID: len(h.data) + 1,
Email: in.Email,
Name: strings.TrimSpace(*in.Name),
CreatedAt: h.waktuDib,
}
h.data = append(h.data, u)
h.emailAda[in.Email] = true
w.Header().Set("Location", fmt.Sprintf("/users/%d", u.ID))
tulisJSON(w, http.StatusCreated, u)
}
func (h *Handler) detail(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
tulisGalat(w, http.StatusBadRequest, "INVALID_ID", "id harus angka")
return
}
for _, u := range h.data {
if u.ID == id {
tulisJSON(w, http.StatusOK, u)
return
}
}
tulisGalat(w, http.StatusNotFound, "NOT_FOUND",
fmt.Sprintf("pengguna dengan id %d tidak ditemukan", id))
}
type galatBody struct {
Error galatRincian `json:"error"`
}
type galatRincian struct {
Code string `json:"code"`
Message string `json:"message"`
}
func tulisGalat(w http.ResponseWriter, status int, kode, pesan string) {
tulisJSON(w, status, galatBody{Error: galatRincian{Code: kode, Message: pesan}})
}
func tulisJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func main() {
h := &Handler{
emailAda: make(map[string]bool),
waktuDib: time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC),
}
mux := h.routes()
// 201: sumber daya baru berhasil dibuat, Location menunjuk padanya.
kirim(mux, "POST", "/users", `{"email":"[email protected]","name":"Budi"}`)
// 409: email sudah terdaftar — konflik duplikat.
kirim(mux, "POST", "/users", `{"email":"[email protected]","name":"Budi Kedua"}`)
// 400: payload JSON sah tetapi field name hilang.
kirim(mux, "POST", "/users", `{"email":"[email protected]"}`)
// 200: pembacaan berhasil.
kirim(mux, "GET", "/users/1", "")
// 404: sumber daya tidak ada.
kirim(mux, "GET", "/users/99", "")
}
// kirim mengeksekusi satu permintaan lalu mencetak dump status, Location, dan body.
func kirim(mux *http.ServeMux, metode, target, body string) {
var pembaca io.Reader
if body != "" {
pembaca = strings.NewReader(body)
}
req := httptest.NewRequest(metode, target, pembaca)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
fmt.Printf("%-5s %-11s -> %d", metode, target, rec.Code)
if loc := rec.Header().Get("Location"); loc != "" {
fmt.Printf(" (Location: %s)", loc)
}
fmt.Println()
if rec.Body.Len() > 0 {
fmt.Printf(" %s", rec.Body.String())
}
}