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 #

StatusNamaKonteks pemakaian pada API
200OKGET dan PUT sukses dengan body; DELETE sukses dengan body ringkas
201CreatedPOST berhasil membuat resource; sertakan header Location
204No Contentsukses tanpa body — khas DELETE dan PUT yang tidak mengembalikan representasi
400Bad Requestpayload rusak, JSON tidak sah, atau parameter tidak valid
401Unauthorizedkredensial tidak ada atau salah — minta autentikasi ulang
403Forbiddensudah terautentikasi, tetapi tidak berhak mengakses resource
404Not Foundresource tidak ada, atau path tidak dikenal
409Conflictkonflik dengan state saat ini, misalnya duplikat data unik
422Unprocessable EntityJSON sah secara sintaks tetapi gagal aturan bisnis (konvensi tim)
429Too Many Requestsbatas laju permintaan terlampaui
500Internal Server Errorkegagalan tak terduga di sisi server
503Service Unavailableserver 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.

go
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:

text
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:

  1. Baris 28-33 (type Handler): selain slice data, handler menyimpan peta emailAda sebagai indeks keunikan email untuk mendeteksi duplikat secara cepat.
  2. Baris 42-47 (cabang 400): decode yang gagal dijawab 400 dengan kode INVALID_JSON — permintaan salah bentuk.
  3. Baris 48-51 (cabang 400 kedua): field name yang hilang dijawab 400 VALIDATION_ERROR; masuk kategori masukan tidak valid, bukan konflik.
  4. Baris 52-56 (cabang 409): email yang sudah terdaftar dijawab 409 EMAIL_TELAH_DIPAKAI dengan pesan yang menyebut email penyebab konflik.
  5. Baris 64-67 (sukses 201): resource disimpan, peta email diperbarui, header Location diset, lalu status 201 dan representasi pengguna dikirim.
  6. Baris 70-84 (detail): loop pencarian menjawab 200 dengan data saat ID ditemukan, dan 404 NOT_FOUND saat tidak.
  7. Baris 86-97 (tulisGalat): satu fungsi menjamin seluruh respons gagal berbagi kerangka {"error":{"code","message"}} yang sama.
  8. 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

status-demo.go

Unduh berkas
go
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())
	}
}