Web Artisan
Beranda

Progress belajar

Modul 1 dari 3

0% 0/3 modul selesai

Setelah selesai, tandai modul ini agar progres kursus tetap rapi.

Progress disimpan lokal di browser ini.

Course · HTTP

Belajar HTTP
Fondasi untuk Backend Developer

Sebelum menyentuh router, ORM, atau framework, seorang backend developer perlu paham satu hal lebih dulu: HTTP adalah kontrak komunikasi yang menghubungkan frontend, mobile, gateway, CDN, dan backend kamu.

Protokol: HTTP/1.1, 2, 3Contoh: Go net/http~95 menit baca
01

Kenapa Backend Developer Harus Menguasai HTTP

HTTP bukan sekadar pemanggilan fungsi, tetapi pertukaran pesan

Saat frontend memanggil fetch("/v1/products"), yang sebenarnya terjadi bukan pemanggilan fungsi biasa. Sebuah pesan teks dikirim lewat jaringan, sampai ke server, lalu server membalas dengan pesan teks lain. Pesan-pesan itu adalah HTTP, dan memahami bentuknya adalah fondasi semua pekerjaan backend.

HTTP (HyperText Transfer Protocol) adalah protokol lapisan aplikasi untuk pertukaran data di web. Semantik intinya distandardkan di RFC 9110. Banyak developer yang berpindah dari frontend ke backend menganggap API sebagai kotak ajaib: panggil sebuah URL, dapat data. Padahal di baliknya ada protokol dengan aturan ketat soal method, status, header, dan body. Begitu kamu memegang backend, kamu yang menulis sisi server kontrak ini, bukan hanya memakainya.

📮Analogi: HTTP seperti surat-menyurat formal

Bayangkan request sebagai surat yang kamu kirim ke kantor: ada tujuan (alamat dan jenis layanan), ada kop surat berisi metadata (header), dan ada isi surat (body). Response adalah balasan resmi: ada stempel hasil (status code), kop balasan (header), dan lampiran jawaban (body). Setiap surat berdiri sendiri, tidak ada ingatan otomatis dari surat sebelumnya.

Tiga sifat HTTP yang wajib menempel sejak awal. Pertama, model client-server: satu pihak meminta (client), satu pihak menjawab (server). Kedua, siklus request-response: satu request memicu tepat satu response. Ketiga, stateless: server tidak menyimpan ingatan antar request secara otomatis, jadi tiap request harus membawa sendiri semua yang dibutuhkan (token, identitas, parameter).

⚠️Stateless bukan berarti tanpa state

Stateless artinya protokol-nya tidak mengingat request sebelumnya. State tetap ada, tetapi disimpan eksplisit: di database, di cookie yang dikirim ulang tiap request, atau di token yang dibawa di header. Kalau kamu mengandalkan variabel global di server untuk mengingat siapa yang login, kamu akan kacau begitu ada dua user atau dua instance server.

sequenceDiagram
  participant FE as Frontend (React)
  participant CDN as CDN / Gateway
  participant API as Go API
  participant DB as PostgreSQL
  FE->>CDN: GET /v1/products
  CDN->>API: GET /v1/products (diteruskan)
  API->>DB: SELECT produk
  DB-->>API: baris produk
  API-->>CDN: 200 OK + JSON
  CDN-->>FE: 200 OK + JSON

Gambar 1. Satu request bisa melewati banyak perantara (CDN, gateway, proxy), tetapi semuanya berbicara dalam bahasa yang sama: HTTP. Tiap perantara membaca dan kadang mengubah pesan yang sama.

Cara tercepat membuat konsep ini nyata adalah mengintip request asli. Di browser, buka DevTools lalu tab Network, klik sebuah request, dan baca panel Headers untuk melihat method, URL, dan header yang dikirim. Di terminal, perintah curl -v mencetak request dan response mentah baris demi baris.

Terminal
curl -v https://example.com/ # Baris diawali > adalah request yang dikirim client. # Baris diawali < adalah response dari server.

Di sisi server, sebuah backend HTTP minimal di Go hanya butuh standard library net/http. Server di bawah ini menjadi peta untuk semua section berikutnya: kita akan terus kembali ke http.Request (sisi baca) dan http.ResponseWriter (sisi tulis).

cmd/api/main.go
package main import ( "fmt" "log" "net/http" ) func main() { mux := http.NewServeMux() // Pola "method path" didukung sejak Go 1.22. mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "ok") }) log.Println("listening on :8080") if err := http.ListenAndServe(":8080", mux); err != nil { log.Fatal(err) } }
🌉Jembatan: dari fetch dan route Laravel ke sisi server

Di React kamu menulis fetch(); di Laravel kamu mendaftarkan route di routes/api.php dan mengembalikan response()->json(). Keduanya bekerja di atas HTTP yang sama. Bedanya, sebagai backend developer kamu sekarang berdiri di sisi server: kamu yang menerima *http.Request dan menulis ke http.ResponseWriter, menentukan sendiri status, header, dan body balasannya.

🧭Peta course ini

Section 02 sampai 03 membongkar bentuk request dan response. Section 04 sampai 08 membahas semantik: method, status, header, JSON, dan URL. Section 09 sampai 12 masuk ke auth, CORS, cache, dan keamanan. Section 13 menyatukan semuanya jadi kontrak API nyata, lalu section 14 sampai 15 merangkum topik lanjutan dan langkah berikutnya ke implementasi Go.

02

Anatomi HTTP Request

Method, target, versi, header, dan body dibaca baris demi baris

Sebuah HTTP request, kalau dilihat mentah di kabel, hanyalah teks dengan struktur tetap. Begitu kamu bisa membaca strukturnya, tidak ada lagi yang misterius soal apa yang dikirim frontend ke backend.

Request punya empat bagian: request line (method, target, versi), deretan header, satu baris kosong, lalu body opsional. Berikut bentuk mentah sebuah request POST yang mengirim JSON.

Request mentah
POST /v1/cart/items HTTP/1.1 Host: api.tokoskincare.id Accept: application/json Content-Type: application/json Authorization: Bearer eyJhbGciOi... Content-Length: 38 {"product_id":42,"qty":2}

Baris pertama, POST /v1/cart/items HTTP/1.1, adalah request line. POST adalah method (niat operasi). /v1/cart/items adalah target: path resource, yang bisa diikuti query string. HTTP/1.1 adalah versi protokol. Sisanya sampai baris kosong adalah header, lalu setelah baris kosong barulah body.

HeaderArahFungsi
HostrequestNama host tujuan. Wajib di HTTP/1.1, dipakai server untuk virtual hosting.
AcceptrequestFormat response yang diharapkan client, misalnya application/json.
Content-TyperequestFormat body yang dikirim client. Wajib ada bila request berisi body.
AuthorizationrequestKredensial, misalnya Bearer <token> untuk JWT.
Content-LengthrequestPanjang body dalam byte, sering diisi otomatis oleh client.

Cara membaca bagian-bagian ini secara langsung adalah dengan curl -v. Tanda > menandai baris request yang dikirim, sehingga kamu bisa mencocokkan tiap bagian dengan teori di atas.

Terminal
curl -v https://api.tokoskincare.id/v1/cart/items \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer TOKEN' \ -d '{"product_id":42,"qty":2}'
flowchart TD
  R["HTTP Request"] --> L["Request line: method + target + versi"]
  R --> H["Headers: Host, Accept, Content-Type, Authorization"]
  R --> B["Baris kosong"]
  R --> Body["Body (opsional): JSON, form, file"]

Gambar 2. Empat bagian request. Baris kosong adalah pemisah wajib antara header dan body.

Di sisi server Go, tiap bagian ini muncul sebagai field dari *http.Request. Method ada di r.Method, target terurai di r.URL, dan header ada di r.Header (sebuah map case-insensitive yang dibaca dengan r.Header.Get).

internal/cart/handler.go
func (h *Handler) AddItem(w http.ResponseWriter, r *http.Request) { method := r.Method // "POST" path := r.URL.Path // "/v1/cart/items" accept := r.Header.Get("Accept") // "application/json" contentType := r.Header.Get("Content-Type") authHeader := r.Header.Get("Authorization") log.Printf("%s %s accept=%s ct=%s auth_present=%t", method, path, accept, contentType, authHeader != "") // Body dibaca dari r.Body (sebuah io.ReadCloser), dibahas di section JSON. }
🌉Jembatan: dari konfig Axios ke request mentah

Di Axios kamu menulis axios.post(url, data, { headers }). Library itu menerjemahkan konfigurasi tersebut menjadi request line, header, dan body persis seperti contoh mentah di atas. Sebagai backend developer kamu menerima hasil terjemahannya di *http.Request, bukan objek konfigurasi yang nyaman.

⚠️Header bersifat case-insensitive

Nama header tidak peka huruf besar kecil. Content-Type, content-type, dan CONTENT-TYPE adalah header yang sama. r.Header.Get di Go sudah menangani ini secara konsisten, jadi jangan membandingkan nama header dengan perbandingan string mentah yang case-sensitive.

03

Anatomi HTTP Response

Status line, header, dan body sebagai jawaban server

Kalau request adalah surat permintaan, response adalah balasan resmi. Strukturnya cermin dari request: status line menggantikan request line, lalu header, baris kosong, dan body.

Baris pertama response disebut status line: versi, status code numerik, dan reason phrase. Berikut response sukses untuk pembuatan resource baru.

Response mentah
HTTP/1.1 201 Created Content-Type: application/json Location: /v1/cart/items/77 {"id":77,"product_id":42,"qty":2}

Tidak semua response punya body, dan itu disengaja. 204 No Content justru benar bila tidak ada data untuk dikembalikan, misalnya setelah DELETE yang berhasil. Mengirim body pada 204 melanggar semantik dan membingungkan client.

StatusArti singkatBody?
200 OKSukses umum, ada data dikembalikanYa
201 CreatedResource baru dibuatBiasanya ya, plus header Location
204 No ContentSukses tanpa data balikTidak
400 Bad RequestRequest rusak atau malformedYa, pesan error
404 Not FoundResource tidak adaYa, pesan error
500 Internal Server ErrorBug atau kegagalan di serverYa, pesan generik

Di Go, menulis response punya urutan idiomatik yang tidak boleh dibalik: set header dulu, panggil WriteHeader(status), baru tulis body. Alasannya teknis. Begitu byte pertama body ditulis, status dan header terlanjur terkirim ke client, sehingga perubahan header setelah itu diabaikan diam-diam.

internal/cart/handler.go
func (h *Handler) AddItem(w http.ResponseWriter, r *http.Request) { item := CartItem{ID: 77, ProductID: 42, Qty: 2} // 1. Set semua header lebih dulu. w.Header().Set("Content-Type", "application/json") w.Header().Set("Location", "/v1/cart/items/77") // 2. Tulis status line. w.WriteHeader(http.StatusCreated) // 201 // 3. Baru tulis body. _ = json.NewEncoder(w).Encode(item) }
sequenceDiagram
  participant H as Handler Go
  participant W as http.ResponseWriter
  participant C as Client
  H->>W: w.Header().Set(...)
  H->>W: w.WriteHeader(201)
  Note over W,C: status + header terkirim ke client
  H->>W: json.Encode(body)
  W-->>C: body mengalir

Gambar 3. Setelah WriteHeader, header tidak bisa diubah lagi. Karena itu set header harus mendahului WriteHeader, dan WriteHeader mendahului penulisan body.

⚠️Jebakan urutan tulis di Go

Memanggil w.Write atau json.Encode lebih dulu akan otomatis mengirim status 200 OK. Bila setelah itu kamu memanggil w.WriteHeader(400), Go mencetak peringatan superfluous WriteHeader call dan status tetap 200. Akibatnya client menerima 200 padahal kamu bermaksud error. Selalu set header, lalu WriteHeader, lalu body.

🌉Jembatan: dari response().json() ke ResponseWriter

Di Express res.status(201).json(data) dan di Laravel response()->json($data, 201) mengurus urutan header lalu body untuk kamu di balik layar. Di Go urutan itu dibuka apa adanya lewat http.ResponseWriter, jadi kamu yang bertanggung jawab memanggilnya dalam urutan yang benar.

04

HTTP Methods dan Semantiknya

Method adalah kontrak niat, bukan dekorasi

Method memberi tahu server apa niat operasinya terhadap resource. Memilih method yang benar bukan soal selera, melainkan kontrak yang dipahami browser, proxy, CDN, dan client lain.

Method utama dan niatnya: GET membaca, POST membuat atau memicu aksi, PUT mengganti representasi penuh, PATCH mengubah sebagian, DELETE menghapus, HEAD membaca header saja tanpa body, dan OPTIONS menanyakan kemampuan resource (dipakai berat oleh preflight CORS di section 10).

Menurut RFC 9110 ada tiga sifat kunci yang melekat pada method. Memahami ketiganya mencegah bug yang sulit dilacak, terutama saat ada retry otomatis dari client atau gateway.

Safe (read-only)

Tidak mengubah state server. Aman dipanggil berulang tanpa efek samping: GET, HEAD, OPTIONS.

Idempotent

Memanggil sekali atau berkali-kali memberi efek akhir yang sama: GET, HEAD, OPTIONS, PUT, DELETE.

Cacheable

Response boleh disimpan dan dipakai ulang. GET dan HEAD cacheable secara default.

Tabel berikut adalah peta yang sering ditanyakan saat interview dan, lebih penting, saat mendesain endpoint. Catat anomali pentingnya: PUT idempotent tetapi PATCH dan POST tidak. Semua method safe otomatis idempotent.

MethodSafeIdempotentCacheableNiat tipikal
GETYaYaYaBaca daftar atau detail
HEADYaYaYaCek keberadaan atau metadata
OPTIONSYaYaTidakTanya kemampuan, preflight CORS
POSTTidakTidakHanya bila eksplisitBuat resource, picu aksi
PUTTidakYaTidakGanti representasi penuh
PATCHTidakTidakTidakUbah sebagian field
DELETETidakYaTidakHapus resource
🔁Kenapa idempotency penting di praktik

Jaringan tidak andal. Client atau gateway sering me-retry request yang timeout. Kalau DELETE /v1/orders/5 dipanggil dua kali karena retry, hasil akhirnya tetap sama: order 5 terhapus, jadi aman. Tetapi POST /v1/checkout yang dipanggil dua kali bisa membuat dua order. Karena POST tidak idempotent, operasi seperti checkout butuh perlindungan tambahan, misalnya idempotency key, yang kita singgung di section 14.

Untuk online shop skincare, pemilihan method memetakan langsung ke aksi pengguna. Hindari godaan menjadikan semua endpoint POST hanya karena ada body.

GET /v1/products Baca daftar produk, safe dan cacheable
GET /v1/products/{id} Baca detail satu produk
POST /v1/cart/items Tambah item ke cart, tidak idempotent
PATCH /v1/cart/items/{id} Ubah qty satu item cart
DELETE /v1/cart/items/{id} Hapus item dari cart, idempotent
POST /v1/checkout Ubah cart jadi order, butuh perlindungan idempotency
🌉Jembatan: dari action controller dan mutation React Query

Di Laravel Route::apiResource memetakan controller ke GET/POST/PUT/PATCH/DELETE sesuai konvensi REST. Di React Query, useQuery cocok untuk operasi GET (safe, cacheable) sedangkan useMutation untuk POST/PUT/PATCH/DELETE. Konvensi keduanya bukan kebetulan, melainkan mengikuti semantik method HTTP yang sama persis seperti tabel di atas.

⚠️Jangan pakai GET untuk operasi yang mengubah state

GET /v1/cart/clear adalah jebakan klasik. Karena GET dianggap safe, browser, prefetcher, atau crawler bisa memanggilnya tanpa diminta user, dan cart pengguna ikut terhapus. Operasi yang mengubah state harus memakai POST, PUT, PATCH, atau DELETE.

05

Status Code untuk API Backend

Memberi tahu client hasil request dengan tepat

Status code adalah cara server memberi tahu client kategori hasil dalam satu angka. Frontend membangun seluruh logika UI di atasnya: kapan menampilkan data, kapan validasi, kapan minta login ulang, kapan menampilkan error server.

Angka pertama menentukan kelas: 2xx sukses, 3xx redirect, 4xx kesalahan client, 5xx kesalahan server. Garis pemisah 4xx dan 5xx penting. 4xx berarti client yang salah (request kurang baik), 5xx berarti server yang gagal walau request sudah benar.

flowchart LR
  S["Status code"] --> X2["2xx Sukses"]
  S --> X3["3xx Redirect"]
  S --> X4["4xx Salah client"]
  S --> X5["5xx Salah server"]
  X2 --> A["200, 201, 204"]
  X4 --> B["400, 401, 403, 404, 409, 422, 429"]
  X5 --> C["500"]

Gambar 4. Empat kelas status. API backend menghabiskan sebagian besar waktunya di 2xx dan 4xx; 5xx idealnya jarang dan selalu jadi alarm.

Untuk API, fokus pada himpunan kode yang sering dipakai. Tabel di bawah mengikuti definisi RFC 9110 Section 15.

KodeNamaKapan dipakai
200OKBaca atau update sukses dengan data balik
201CreatedResource baru dibuat, sertakan header Location
204No ContentSukses tanpa body, misalnya DELETE
400Bad RequestSintaks request rusak, JSON tidak valid, header wajib hilang
401UnauthorizedBelum terautentikasi atau token tidak valid
403ForbiddenSudah login tetapi tidak punya hak akses
404Not FoundResource tidak ditemukan
409ConflictKonflik state, misalnya slug produk sudah dipakai
422Unprocessable ContentSintaks benar tetapi gagal validasi semantik atau aturan bisnis
429Too Many RequestsRate limit terlampaui, biasanya dengan Retry-After
500Internal Server ErrorBug atau kegagalan tak terduga di server

Perbedaan 400 dan 422 adalah salah satu yang paling sering keliru. 400 untuk sintaks rusak: body bukan JSON valid, atau header wajib hilang. 422 untuk kasus ketika sintaks benar dan server paham payload, tetapi data melanggar aturan, misalnya qty bernilai nol atau email tidak berformat email. Konvensi API modern memakai 422 untuk error validasi semantik (lihat MDN 422).

400 Bad Request
  • Server tidak bisa memahami request: JSON rusak, tipe salah total.
  • Body bukan JSON padahal Content-Type: application/json.
  • Parameter wajib di query atau path hilang.
422 Unprocessable Content
  • Server paham payload, tetapi nilainya melanggar aturan bisnis.
  • qty: 0, harga negatif, email tidak valid.
  • Cocok untuk daftar error per field di form frontend.

Gunakan 409 Conflict untuk konflik state yang bukan salah format, misalnya mencoba membuat produk dengan slug yang sudah ada, atau checkout cart yang sudah pernah di-checkout. Untuk konsistensi, susun matriks status per area sebelum menulis kode.

AreaSuksesError tipikal
Login200 + token401 kredensial salah, 429 brute force
Detail produk200404 produk tidak ada
Tambah ke cart201 atau 200422 qty invalid, 404 produk tidak ada
Checkout201 order409 cart sudah checkout, 422 stok kurang

Di Go, status ditulis lewat konstanta http.Status... agar terbaca jelas, bukan angka mentah.

internal/cart/handler.go
func (h *Handler) AddItem(w http.ResponseWriter, r *http.Request) { var req AddItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "body bukan JSON yang valid") return } if req.Qty <= 0 { writeError(w, http.StatusUnprocessableEntity, "qty harus lebih dari 0") return } item, err := h.svc.AddItem(r.Context(), req.ProductID, req.Qty) if errors.Is(err, ErrProductNotFound) { writeError(w, http.StatusNotFound, "produk tidak ditemukan") return } if err != nil { writeError(w, http.StatusInternalServerError, "terjadi kesalahan di server") return } writeJSON(w, http.StatusCreated, item) }
🌉Jembatan: dari try/catch ke status code

Di frontend kamu menulis if (res.status === 401) redirectToLogin() atau menangkap error dari res.ok. Backend developer berada di sisi yang menetapkan angka itu. Status code yang konsisten dari server membuat logika try/catch dan kondisi UI di frontend jadi sederhana dan dapat diprediksi.

⚠️Jangan membalas 200 untuk error

Pola lama membalas 200 OK dengan body {"success": false} untuk semua kondisi, termasuk error. Ini menyulitkan client, caching, dan monitoring karena mereka mengandalkan status code. Pakai kode yang benar: 4xx untuk salah client, 5xx untuk salah server.

06

Header yang Penting untuk Backend

Metadata yang mengatur format, auth, cache, redirect, dan tracing

Header adalah metadata pesan, terpisah dari body. Mereka mengatur banyak hal di luar isi data: format, autentikasi, cache, redirect, dan penelusuran. Salah set sebagian header hanya bikin tidak rapi; salah set sebagian lain bisa membuka celah keamanan.

HeaderPeranCatatan
Content-TypeFormat bodyWajib bila ada body, misalnya application/json
AcceptFormat response yang diminta clientDasar content negotiation
AuthorizationKredensialBearer <token>, jangan pernah di-log mentah
Cache-ControlAturan cachepublic versus private, max-age, no-store
ETagSidik jari versi resourceDasar conditional request dan 304
LocationAlamat resource baru atau tujuan redirectDipakai pada 201 dan 3xx
Set-CookieServer menanam cookie di clientAtur HttpOnly, Secure, SameSite
X-Request-IDPenanda unik per requestUntuk tracing dan korelasi log

Tidak semua header setara. Sebagian wajib (request berisi body tanpa Content-Type itu ambigu), sebagian opsional, dan sebagian berbahaya bila salah. Cache-Control yang salah bisa membuat data privat tersimpan di cache bersama; Set-Cookie tanpa HttpOnly membuka cookie ke JavaScript dan rawan dicuri.

⚠️Header berbahaya bila salah set

Tiga contoh yang sering jadi masalah: Cache-Control: public pada response berisi data privat (cart, profil) bisa membocorkan data ke cache bersama; Set-Cookie tanpa Secure mengirim cookie lewat HTTP polos; dan Authorization yang ikut tercetak di access log membocorkan token. Perlakukan header ini dengan hati-hati.

Lihat header pada dua flow nyata. Pertama, login yang menanam cookie session. Server membalas dengan Set-Cookie berisi atribut keamanan.

Response login
HTTP/1.1 200 OK Content-Type: application/json Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/ {"user":{"id":7,"name":"Tika"}}

Kedua, pembuatan resource yang mengembalikan Location agar client tahu alamat resource baru tanpa menebak.

internal/order/handler.go
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { order, err := h.svc.Checkout(r.Context(), customerID(r)) if err != nil { writeError(w, http.StatusInternalServerError, "checkout gagal") return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Location", fmt.Sprintf("/v1/orders/%d", order.ID)) w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(order) }
🔎X-Request-ID untuk tracing

Header dengan awalan X- adalah konvensi non-standar yang banyak dipakai, salah satunya X-Request-ID. Idenya: gateway atau middleware menyuntik satu id unik ke tiap request, lalu id itu ikut di setiap baris log dan diteruskan ke service lain. Saat ada laporan bug, kamu cukup mencari satu id untuk merunut seluruh perjalanan request. Kita bahas lebih dalam di section 14.

🌉Jembatan: dari interceptor Axios ke header asli

Interceptor Axios atau middleware Laravel sering menambah header Authorization atau X-Request-ID ke setiap request secara otomatis. Yang benar-benar terkirim tetaplah header HTTP biasa. Memahami header level protokol membuatmu tidak bergantung pada abstraksi library saat harus mendebug request yang aneh.

07

JSON dan Content Negotiation

Kontrak payload eksplisit antara client dan server

Mayoritas API backend modern bertukar data dalam JSON. Tetapi JSON bukan default ajaib; ia harus dinyatakan eksplisit lewat header agar client dan server sepakat soal format.

Dua header mengatur kesepakatan ini. Content-Type: application/json pada request menyatakan body yang dikirim berbentuk JSON. Accept: application/json menyatakan client mengharapkan response JSON. Mekanisme server memilih format berdasarkan Accept disebut content negotiation. Untuk API yang hanya bicara JSON, negosiasinya sederhana, tetapi tetap baik untuk memvalidasi Content-Type request.

sequenceDiagram
  participant FE as Frontend
  participant API as Go API
  FE->>API: POST /v1/cart/items (Content-Type: application/json)
  Note over API: cek Content-Type, decode body ke struct
  API-->>FE: 201 Created (Content-Type: application/json)

Gambar 5. Kontrak dua arah: request mendeklarasikan format body lewat Content-Type, server mendeklarasikan format response lewat Content-Type response.

Di Go, parsing body JSON memakai json.NewDecoder(r.Body).Decode(&v). Penanganan yang baik tidak berhenti di happy path; ia menangani JSON rusak, body kosong, dan content type yang salah. Secara default decoder mengabaikan field yang tidak dikenal, tetapi DisallowUnknownFields membuatnya menolak field asing, berguna untuk kontrak yang ketat.

internal/cart/handler.go
type AddItemRequest struct { ProductID int64 `json:"product_id"` Qty int `json:"qty"` } func (h *Handler) AddItem(w http.ResponseWriter, r *http.Request) { // 1. Tolak content type yang bukan JSON. if ct := r.Header.Get("Content-Type"); ct != "" && ct != "application/json" { writeError(w, http.StatusUnsupportedMediaType, "Content-Type harus application/json") return } // 2. Decode dengan menolak field asing. dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() var req AddItemRequest if err := dec.Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "JSON tidak valid: "+err.Error()) return } // 3. Validasi semantik terpisah dari parsing. if req.ProductID <= 0 || req.Qty <= 0 { writeError(w, http.StatusUnprocessableEntity, "product_id dan qty harus lebih dari 0") return } writeJSON(w, http.StatusCreated, req) }
💡Pisahkan parsing dari validasi

Kegagalan decode (JSON rusak) adalah masalah sintaks, balas 400. Nilai yang lolos decode tetapi melanggar aturan (qty nol) adalah masalah semantik, balas 422. Dua jenis kegagalan ini sengaja dipisah karena memberi sinyal berbeda ke client: yang satu bug di pengiriman, yang lain data yang salah.

🌉Jembatan: dari objek JS ke struct Go

Di JavaScript objek bersifat dinamis: field bisa ada atau tidak, bertipe apa saja. Struct Go bersifat statis: field dan tipenya tetap. json.Unmarshal memetakan key JSON ke field struct lewat tag json:"...". Field JSON yang tidak punya pasangan di struct diabaikan secara default. Inilah kontrak eksplisit yang menggantikan kebebasan objek JS.

⚠️Hati-hati unknown field yang diam-diam terbuang

Tanpa DisallowUnknownFields, client yang salah ketik nama field, misalnya quantity alih-alih qty, tidak akan dapat error. Field salah itu diabaikan, Qty tetap zero value (0), dan request lolos parsing tetapi berperilaku aneh. Untuk endpoint penting seperti checkout, mengaktifkan DisallowUnknownFields membantu menangkap kesalahan kontrak lebih awal.

08

URL, Path Param, dan Query Param

Identitas resource versus parameter baca dan filter

Satu URL membawa dua jenis informasi yang sering tertukar: identitas resource dan parameter pemfilteran. Memisahkan keduanya dengan benar membuat desain endpoint bersih dan dapat diprediksi.

Path parameter adalah bagian dari identitas resource: GET /products/42 mengidentifikasi produk dengan id 42. Query parameter adalah opsi baca, filter, atau urutan yang tidak mengubah identitas: GET /products?skin_type=oily&page=1 tetap menunjuk koleksi produk, hanya menyaringnya. Aturan praktisnya: kalau menghapus parameter mengubah resource apa yang dimaksud, itu path; kalau hanya mengubah cara menyaring atau menampilkan, itu query.

Path param: identitas
  • GET /v1/products/42 menunjuk satu produk spesifik.
  • GET /v1/orders/1001 menunjuk satu order.
  • Menghapus id mengubah arti URL sepenuhnya.
Query param: filter dan baca
  • ?skin_type=oily menyaring berdasarkan jenis kulit.
  • ?page=2&per_page=20 mengatur pagination.
  • ?sort=price_asc mengatur urutan, tanpa mengubah koleksi.

Untuk daftar yang besar, tiga pola query standar muncul berulang: pagination (page, per_page), filtering (skin_type, category), dan sorting (sort). Nilai query yang mengandung karakter khusus harus di-URL-encode, misalnya spasi menjadi %20. Standard library Go menangani encoding dan decoding ini di balik r.URL.Query.

GET /v1/products/{id} Identitas: detail satu produk lewat path param
GET /v1/products?skin_type=oily&page=1&sort=price_asc Filter: daftar produk tersaring lewat query param

Di Go, path value dibaca dengan r.PathValue("id") (sejak Go 1.22 di net/http, atau chi.URLParam bila memakai chi). Query dibaca dengan r.URL.Query().Get("skin_type"). Keduanya selalu mengembalikan string, jadi nilai numerik perlu di-parse dan divalidasi.

internal/product/handler.go
func (h *Handler) GetByID(w http.ResponseWriter, r *http.Request) { // Path param: identitas resource. id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil || id <= 0 { writeError(w, http.StatusBadRequest, "id produk tidak valid") return } _ = id // lanjut ambil produk dari service } func (h *Handler) List(w http.ResponseWriter, r *http.Request) { // Query param: filter dan pagination. q := r.URL.Query() skinType := q.Get("skin_type") // "" bila tidak diisi page, err := strconv.Atoi(q.Get("page")) if err != nil || page < 1 { page = 1 // default aman bila kosong atau invalid } log.Printf("filter skin_type=%q page=%d", skinType, page) }
🌉Jembatan: dari React Router params ke path value

Di React Router useParams() membaca path param, dan useSearchParams() membaca query string. Di Laravel, route model binding menyuntik path param sebagai argumen controller. r.PathValue dan r.URL.Query di Go adalah padanan level protokolnya, hanya saja kamu yang melakukan parsing dan validasi secara eksplisit.

💡Beri default yang aman untuk query

Query parameter bersifat opsional. page yang kosong sebaiknya jatuh ke 1, bukan menyebabkan error atau page=0. Selalu sediakan nilai default yang masuk akal dan batasi per_page ke maksimum tertentu (misalnya 100) agar client tidak meminta sepuluh ribu baris sekaligus.

09

Cookie, Session, dan Header Autentikasi

Cookie-based session versus bearer token

HTTP stateless, jadi server tidak otomatis tahu siapa pemanggilnya. Identitas harus dibawa di tiap request. Ada dua gaya utama di level HTTP: cookie-based session dan bearer token.

Pada cookie-based session, server menanam cookie lewat Set-Cookie setelah login. Browser otomatis mengirim cookie itu kembali di setiap request berikutnya lewat header Cookie. Pada bearer token, server mengembalikan token (sering JWT), dan client menyertakannya secara manual di header Authorization: Bearer <token> pada tiap request.

flowchart TD
  L["POST /login"] --> S["Server verifikasi kredensial"]
  S --> CK["Set-Cookie: session=...<br/>HttpOnly Secure SameSite"]
  S --> TK["body: access_token=..."]
  CK --> B["Browser kirim Cookie otomatis"]
  TK --> C["Client kirim Authorization: Bearer ... manual"]

Gambar 6. Dua jalur auth. Cookie dikirim browser otomatis; bearer token dikirim client secara manual di header Authorization.

Keamanan cookie ditentukan oleh atributnya. HttpOnly mencegah JavaScript membaca cookie (memitigasi pencurian token via XSS). Secure membuat cookie hanya dikirim lewat HTTPS. SameSite (Strict, Lax, atau None) membatasi pengiriman cookie pada navigasi lintas situs, yang memitigasi CSRF. Detail atribut ini ada di MDN Cookies.

CSRF (Cross-Site Request Forgery)

Serangan ketika situs jahat memancing browser korban mengirim request ke situs lain tempat korban sudah login, memanfaatkan cookie yang dikirim browser secara otomatis. Atribut SameSite pada cookie adalah mitigasi utama di level HTTP.

Di Go, menanam cookie aman memakai http.SetCookie dengan atribut yang lengkap, dan membaca bearer token cukup memeriksa prefix header Authorization.

internal/auth/handler.go
func setSessionCookie(w http.ResponseWriter, token string) { http.SetCookie(w, &http.Cookie{ Name: "session", Value: token, Path: "/", HttpOnly: true, // tidak terbaca JavaScript Secure: true, // hanya lewat HTTPS SameSite: http.SameSiteLaxMode, // mitigasi CSRF MaxAge: 3600, }) } func bearerToken(r *http.Request) (string, bool) { const prefix = "Bearer " h := r.Header.Get("Authorization") if len(h) <= len(prefix) || h[:len(prefix)] != prefix { return "", false } return h[len(prefix):], true }
Cookie session
  • Dikirim browser otomatis, nyaman untuk web di domain yang sama.
  • Aman dari XSS bila HttpOnly, tetapi perlu mitigasi CSRF (SameSite).
  • Cocok untuk aplikasi web klasik dan SPA satu domain.
Bearer token
  • Dikirim manual di header, fleksibel untuk mobile dan lintas domain.
  • Bebas CSRF (tidak otomatis dikirim), tetapi rawan bila disimpan di tempat yang terbaca JS.
  • Cocok untuk API publik, mobile, dan service-to-service.
🌉Jembatan: dari session Laravel dan login SPA

Login Laravel klasik memakai session cookie yang dikelola framework. SPA yang memanggil API publik lebih sering memakai bearer token. Keduanya hanyalah dua cara membawa identitas di atas HTTP yang stateless. Memahami pilihan ini di level header membuatmu bisa memilih pendekatan, bukan sekadar mengikuti default library.

⚠️Token bukan tempat menyimpan rahasia tanpa proteksi

JWT yang ditandatangani (signed) bisa diverifikasi, tetapi isinya dapat dibaca siapa saja yang memegangnya kecuali dienkripsi. Jangan menaruh data sensitif di payload token. Dan jangan menyimpan bearer token di localStorage tanpa sadar risikonya, karena di sana ia terbaca oleh JavaScript dan rawan XSS.

10

CORS untuk Frontend dan Backend

Aturan keamanan browser, bukan fitur backend biasa

CORS sering disalahpahami sebagai bug backend atau masalah Postman. Sebenarnya CORS adalah aturan keamanan yang ditegakkan oleh browser. Itulah kenapa request yang sama berhasil di curl atau Postman, tetapi diblokir di browser.

Browser menerapkan same-origin policy: secara default JavaScript hanya boleh membaca response dari origin yang sama (kombinasi skema, host, dan port). Saat frontend di http://localhost:3000 memanggil API di http://localhost:8080, itu lintas origin, dan browser butuh izin eksplisit dari server lewat header CORS sebelum mengizinkan JavaScript membaca responsnya. Aturan lengkapnya ada di MDN CORS.

Untuk request tertentu (method non-sederhana atau header kustom), browser mengirim preflight lebih dulu: sebuah request OPTIONS tanpa kredensial yang menanyakan apakah request asli diizinkan. Hanya bila preflight dijawab dengan header izin yang benar, browser melanjutkan request asli.

sequenceDiagram
  participant JS as JS di localhost:3000
  participant BR as Browser
  participant API as API localhost:8080
  JS->>BR: fetch POST /v1/cart/items
  BR->>API: OPTIONS /v1/cart/items (preflight)
  API-->>BR: 204 + Access-Control-Allow-Origin/Methods/Headers
  BR->>API: POST /v1/cart/items (request asli)
  API-->>BR: 200 + Access-Control-Allow-Origin
  BR-->>JS: response boleh dibaca

Gambar 7. Preflight OPTIONS berjalan sebelum request asli. Bila header izin tidak cocok, browser memblokir dan JavaScript menerima error CORS, bukan response.

Ada aturan keras soal kredensial: Access-Control-Allow-Origin: * (wildcard) TIDAK boleh dipakai bersama kredensial (cookie atau Authorization). Bila request membawa kredensial, server harus meng-echo nilai Origin yang spesifik dan menambah Vary: Origin agar cache tidak tertukar antar origin. Ini didokumentasikan di MDN CORS credentials.

internal/middleware/cors.go
func CORS(allowedOrigin string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin == allowedOrigin { // Echo origin spesifik, bukan wildcard, karena kita pakai kredensial. w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Add("Vary", "Origin") } if r.Method == http.MethodOptions { // Jawab preflight. w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } }
⚠️Wildcard plus kredensial selalu gagal

Mengirim Access-Control-Allow-Origin: * bersama Access-Control-Allow-Credentials: true akan ditolak browser, dan request gagal walau server merespons 200. Bila API kamu memakai cookie atau Authorization, kamu wajib meng-echo origin spesifik. Wildcard hanya cocok untuk API publik tanpa kredensial sama sekali.

🛠️Cara debug error CORS

Saat melihat error CORS merah di console, jangan asal pasang wildcard. Buka DevTools Network, cari request OPTIONS preflight, dan periksa response header-nya: apakah Access-Control-Allow-Origin cocok dengan origin frontend, apakah method dan header yang dipakai ada di Allow-Methods dan Allow-Headers. Error CORS hampir selalu soal salah satu header izin yang kurang atau tidak cocok.

🌉Jembatan: kenapa Postman tidak kena CORS

Postman dan curl bukan browser, jadi mereka tidak menerapkan same-origin policy. Inilah sebabnya request berhasil di Postman tetapi gagal di aplikasi React. CORS murni mekanisme browser; backend hanya menyediakan header izin, browser yang menegakkannya.

11

HTTP Caching untuk API

Mempercepat baca tanpa membocorkan data

Caching HTTP bisa membuat API baca jauh lebih cepat dan murah, tetapi cache yang salah bisa menampilkan stok lama atau membocorkan data privat ke pengguna lain. Aturannya distandardkan di RFC 9111.

Header Cache-Control adalah kemudi utama. public membolehkan cache bersama (proxy, CDN) menyimpan response; private membatasinya ke cache milik satu pengguna (browser). max-age=N menyatakan berapa detik response dianggap segar. no-store melarang penyimpanan sama sekali, dipakai untuk data sensitif atau yang harus selalu segar.

DirectiveArtiCocok untuk
public, max-age=3600Boleh di-cache siapa saja selama 1 jamDaftar kategori, detail produk
private, max-age=60Hanya cache browser, 1 menitData per-user yang jarang berubah
no-storeTidak boleh disimpan di mana punCart, inventory, status order

Selain durasi, ada validasi. ETag adalah sidik jari versi resource. Client menyimpan ETag, lalu pada request berikutnya mengirim If-None-Match: <etag>. Bila resource tidak berubah, server membalas 304 Not Modified tanpa body, dan client memakai ulang salinan yang sudah ada. Ini menghemat bandwidth meski tetap ada satu round-trip. Mekanisme serupa berlaku untuk Last-Modified dengan If-Modified-Since.

sequenceDiagram
  participant C as Client
  participant API as Go API
  C->>API: GET /v1/products/42
  API-->>C: 200 OK + ETag: "v7"
  Note over C: simpan response + ETag
  C->>API: GET /v1/products/42 (If-None-Match: "v7")
  alt resource tidak berubah
    API-->>C: 304 Not Modified (tanpa body)
  else berubah
    API-->>C: 200 OK + ETag: "v8" + body baru
  end

Gambar 8. Conditional request dengan ETag. Bila tidak berubah, server cukup membalas 304 tanpa mengirim ulang body.

Di Go, set Cache-Control dan ETag untuk endpoint baca, lalu bandingkan If-None-Match untuk membalas 304 bila cocok.

internal/product/handler.go
func (h *Handler) GetByID(w http.ResponseWriter, r *http.Request) { product, etag := h.svc.GetWithETag(r.Context(), 42) w.Header().Set("Cache-Control", "public, max-age=3600") w.Header().Set("ETag", etag) if match := r.Header.Get("If-None-Match"); match == etag { w.WriteHeader(http.StatusNotModified) // 304, tanpa body return } writeJSON(w, http.StatusOK, product) }

Yang paling kritikal adalah memilih endpoint mana yang aman di-cache. Aman: data publik dan jarang berubah seperti detail produk dan daftar kategori. Tidak aman di cache browser, proxy, atau CDN: data yang sering berubah atau per-pengguna seperti cart, inventory (stok real-time), dan status order. Untuk yang tidak aman, pakai no-store.

⚠️Cart dan stok jangan pernah public-cache

Membuat GET /v1/cart ber-Cache-Control: public adalah bencana: cache bersama bisa menyajikan cart milik user A ke user B. Stok yang di-cache lama membuat pelanggan checkout barang yang sebenarnya habis. Untuk data per-user dan real-time, pakai no-store atau setidaknya private dengan max-age sangat pendek.

🌉Jembatan: dari cache React Query ke cache HTTP

React Query menyimpan hasil query di memori klien dan punya konsep staleTime. Cache HTTP bekerja satu lapis lebih bawah dan lebih luas: di browser, proxy, dan CDN, dikendalikan oleh header dari server. Keduanya bisa berjalan bersama. Memahami cache HTTP membuatmu mengontrol cache di seluruh jalur, bukan hanya di komponen React.

12

HTTPS, TLS, dan Security Headers

Baseline keamanan transport sebelum auth dan payment

Sebelum bicara auth dan pembayaran, ada baseline yang harus terpasang: enkripsi transport lewat HTTPS dan beberapa security header. Penting juga memahami apa yang HTTPS lindungi dan apa yang tidak.

HTTPS adalah HTTP yang berjalan di atas TLS (Transport Layer Security). Yang dilindungi adalah transport: kerahasiaan (data tidak bisa dibaca penyadap di jaringan) dan integritas (data tidak bisa diubah diam-diam di jalan), plus autentikasi server lewat sertifikat. Yang tidak dilindungi HTTPS: bug di aplikasi kamu, SQL injection, otorisasi yang lemah, atau data yang bocor setelah sampai di server. HTTPS mengamankan jalur, bukan logika aplikasi.

🚚Analogi: HTTPS itu truk lapis baja

HTTPS seperti truk lapis baja yang membawa paket dengan aman dari pengirim ke penerima. Tidak ada yang bisa mengintip atau menukar isi paket selama perjalanan. Tetapi truk lapis baja tidak menjamin isi paketnya benar, atau bahwa penerima berhak menerimanya. Kebenaran isi dan hak akses tetap urusan aplikasi.

Di atas HTTPS, beberapa security header memberi instruksi keamanan ke browser. Baseline yang umum diterapkan.

HeaderFungsi
Strict-Transport-SecurityHSTS: paksa browser selalu memakai HTTPS untuk domain ini, dengan max-age
X-Content-Type-Options: nosniffLarang browser menebak MIME, mencegah eksekusi response sebagai script
Content-Security-PolicyBatasi sumber konten (script, style, img) untuk memitigasi XSS

Strict-Transport-Security membuat browser menolak koneksi HTTP polos ke domain itu setelah kunjungan pertama (lihat MDN HSTS). X-Content-Type-Options: nosniff mencegah browser menebak tipe konten dan menjalankan response non-script sebagai JavaScript (lihat MDN nosniff). Content-Security-Policy adalah pertahanan kuat terhadap XSS tetapi perlu dikonfigurasi hati-hati sesuai aset aplikasi.

internal/middleware/security.go
func SecurityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Security-Policy", "default-src 'self'") next.ServeHTTP(w, r) }) }
🔐Lengkapi cookie dengan Secure di produksi

Begitu HTTPS aktif, pastikan semua cookie sensitif memakai atribut Secure agar tidak pernah terkirim lewat HTTP polos. Gabungkan dengan HttpOnly dan SameSite dari section 09. HTTPS plus cookie ber-Secure adalah baseline minimum sebelum menangani login dan payment.

⚠️HSTS itu lengket, uji dulu

Setelah browser menerima HSTS dengan max-age panjang, ia akan menolak HTTP polos ke domain itu selama durasi tersebut, sulit dibatalkan. Pakai max-age kecil saat menguji, baru naikkan ke setahun setelah yakin seluruh domain dan subdomain benar-benar melayani HTTPS.

13

Merancang API HTTP Nyata

Menyatukan semua konsep jadi satu kontrak API online shop

Sekarang semua konsep digabung menjadi satu kontrak API yang utuh untuk online shop skincare: resource, method, status, header, body, auth, cache, dan format error yang konsisten. Inilah jembatan dari membaca HTTP ke mengimplementasikannya di Go.

Mulai dari resource design. Resource utama: produk, cart, order, dan auth. Tiap resource mengekspos operasi lewat method yang tepat, bukan semuanya POST.

POST /v1/auth/login Login, balas token, 200 atau 401
GET /v1/products Daftar produk dengan filter dan pagination, public-cacheable
GET /v1/products/{id} Detail produk, ETag dan Cache-Control
GET /v1/cart Isi cart milik user, no-store
POST /v1/cart/items Tambah item ke cart, 201 atau 422
PATCH /v1/cart/items/{id} Ubah qty item cart
DELETE /v1/cart/items/{id} Hapus item dari cart, 204
POST /v1/checkout Ubah cart jadi order, 201 atau 409, butuh idempotency
GET /v1/orders Riwayat order user, no-store
GET /v1/orders/{id} Detail satu order

Selanjutnya, petakan halaman frontend ke endpoint. Pemetaan ini membantu memastikan tiap kebutuhan UI punya endpoint yang jelas dengan semantik HTTP yang benar.

Halaman frontendEndpointCatatan HTTP
Daftar produkGET /v1/products?skin_type=oily&page=1200, public cache, query filter
Detail produkGET /v1/products/{id}200 atau 404, ETag
CartGET /v1/cart, POST /v1/cart/itemsno-store, butuh auth
CheckoutPOST /v1/checkout201 atau 409, idempotency key
Riwayat orderGET /v1/ordersno-store, butuh auth

Kunci konsistensi adalah format error yang seragam. Tetapkan satu bentuk error untuk seluruh API agar frontend menanganinya dengan satu jalur kode. Bentuk minimal: kode status di HTTP, plus body berisi pesan dan opsional detail per field untuk 422.

Format error 422
{ "error": { "message": "validasi gagal", "fields": { "qty": "harus lebih dari 0" } } }

Di Go, kontrak ini diwujudkan dengan helper kecil yang konsisten dipakai di semua handler, plus struct request dan response yang eksplisit. Perhatikan field JSON memakai snake_case sementara struct Go memakai PascalCase lewat tag, dan uang memakai int64 (PriceRupiah), bukan float.

internal/httpx/respond.go
package httpx import ( "encoding/json" "net/http" ) type ErrorBody struct { Message string `json:"message"` Fields map[string]string `json:"fields,omitempty"` } func JSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) } func Error(w http.ResponseWriter, status int, message string, fields map[string]string) { JSON(w, status, map[string]any{ "error": ErrorBody{Message: message, Fields: fields}, }) }
internal/product/model.go
package product type Product struct { ID int64 `json:"id"` Name string `json:"name"` Slug string `json:"slug"` Category string `json:"category"` PriceRupiah int64 `json:"price"` Stock int `json:"stock"` Status string `json:"status"` }

Struktur folder yang menampung semua ini tetap rapi dengan memisahkan per domain plus satu paket helper HTTP bersama.

Struktur API online shop
  • cmd/
  • api/
  • main.go entry point, wiring router dan middleware
  • internal/
  • httpx/
  • respond.go helper JSON dan Error konsisten
  • middleware/
  • cors.go header CORS
  • security.go security header dan HSTS
  • auth/
  • handler.go login, cookie atau bearer token
  • product/
  • handler.go katalog, ETag, cache
  • model.go
  • cart/
  • handler.go cart per-user, no-store
  • order/
  • handler.go checkout dan riwayat order
  • go.mod
flowchart LR
  FE["Frontend / Mobile"] -->|HTTPS JSON| MW["Middleware: CORS, Security, Auth"]
  MW --> R["Router net/http"]
  R --> P["Product handler"]
  R --> C["Cart handler"]
  R --> O["Order handler"]
  R --> A["Auth handler"]

Gambar 9. Request menembus middleware lintas-cutting (CORS, security, auth) lalu sampai ke handler per domain. Tiap handler menerapkan status, header, dan cache sesuai semantik resource-nya.

🌉Jembatan: dari konsumsi API ke kepemilikan API

Sebagai frontend developer kamu mengonsumsi kontrak ini lewat React, Product Detail, Cart, dan Checkout. Sekarang kamu yang merancang dan memilikinya: menentukan method, status, header, cache, dan format error. Setelah kontrak ini jelas, implementasi net/http dan chi tinggal mengisinya.

14

Topik Lanjutan dan Langkah Berikutnya

Versi HTTP, observability, dan apa yang sengaja ditunda

Beberapa topik sengaja ditunda agar fondasi tidak kabur, tetapi penting untuk diketahui arahnya. Dua yang utama: versi HTTP (cara byte dikirim) dan observability (cara menelusuri request di produksi).

Semantik HTTP (method, status, header) tetap sama lintas versi. Yang berbeda hanyalah cara byte dikirim di kabel. HTTP/1.1 memakai koneksi yang bisa dipakai ulang (keep-alive) tetapi memproses request relatif berurutan per koneksi. HTTP/2 menambah multiplexing: banyak request berbagi satu koneksi, tetapi masih bisa kena head-of-line blocking di lapisan TCP karena satu paket hilang menahan semua stream. HTTP/3, dipublikasikan sebagai RFC 9114 pada 7 Juni 2022, mengganti TCP dengan QUIC (transport berbasis UDP) sehingga paket hilang hanya memengaruhi stream-nya sendiri.

VersiTransportCiri utama
HTTP/1.1TCPConnection reuse (keep-alive), request relatif berurutan
HTTP/2TCPMultiplexing satu koneksi, masih kena HOL blocking di TCP
HTTP/3QUIC (UDP)Multiplexing native, tanpa HOL blocking antar-stream

Cara cek versi: di DevTools tab Network, tambahkan kolom Protocol (h2 untuk HTTP/2, h3 untuk HTTP/3). Di terminal, curl --http2 -I https://example.com memaksa percobaan HTTP/2.

Terminal
curl --http2 -I https://example.com # Baris pertama menunjukkan versi yang dipakai, mis. HTTP/2 200.
🧱Jangan campur desain REST dengan versi transport

Pemilihan method, status, dan struktur resource tidak berubah karena kamu beralih dari HTTP/1.1 ke HTTP/2 atau HTTP/3. Desain API kamu tetap sama; yang berubah hanya performa transport. Pisahkan dua keputusan ini di kepala.

Topik kedua adalah observability: kemampuan menelusuri request dari masuk sampai keluar di produksi. Tiga pilar praktisnya: access log terstruktur (method, path, status, latency, user agent, IP), request id per request, dan correlation id untuk menelusuri satu transaksi lintas service. Saat ada bug pada POST /checkout atau callback payment, satu id memungkinkan kamu merunut seluruh jejaknya.

internal/middleware/logger.go
func AccessLog(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rw := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rw, r) // Structured log: field minimal untuk debugging produksi. log.Printf("method=%s path=%s status=%d latency_ms=%d request_id=%s", r.Method, r.URL.Path, rw.status, time.Since(start).Milliseconds(), r.Header.Get("X-Request-ID")) }) }
🌉Jembatan: dari console.log ke structured logging

Di frontend kamu terbiasa console.log saat debugging. Di backend produksi, log harus terstruktur (key-value) agar bisa dicari dan diagregasi oleh tooling. Bukan sekadar mencetak teks, melainkan mencatat field konsisten (method, path, status, latency, request id) yang bisa dirunut belakangan.

Dua hal lain yang sengaja ditahan sampai masuk implementasi backend Go: idempotency lanjutan (idempotency key untuk POST /checkout agar retry tidak membuat order ganda) dan rate limiting (429 Too Many Requests dengan header Retry-After). Keduanya dibahas lebih dalam saat membangun backend nyata.

🧭Arah belajar berikutnya

Fondasi HTTP ini bermuara ke implementasi server Go: handler net/http, router dan middleware (lihat course routing dengan chi), lalu REST API lengkap dengan database, auth, dan deploy. Semantik HTTP yang kamu kuasai di sini akan terbawa di setiap langkah itu.

15

Ringkasan dan Poin Penting

Checklist request-response dan satu flow lengkap

HTTP adalah fondasi sebelum router, handler, middleware, database, auth, dan deploy. Menutup course ini, mari satukan semuanya jadi checklist yang dipakai berulang dan satu flow checkout yang merekatkan tiap konsep.

Checklist berikut adalah lensa yang bisa kamu tempelkan ke setiap endpoint yang kamu rancang.

Checklist Request-Response

  • Method: pilih sesuai niat dan sifatnya (safe, idempotent, cacheable), bukan asal POST.
  • Status: 2xx sukses, 4xx salah client (400 sintaks, 422 validasi, 401 auth, 404 tidak ada, 409 konflik, 429 rate limit), 5xx salah server.
  • Header: Content-Type dan Accept untuk format, Location pada 201, Cache-Control dan ETag untuk cache.
  • Body: JSON eksplisit, parsing dipisah dari validasi, 204 berarti tanpa body.
  • Auth: cookie session (HttpOnly, Secure, SameSite) atau bearer token; identitas dibawa tiap request.
  • CORS: aturan browser; echo origin spesifik bila pakai kredensial, jangan wildcard plus kredensial.
  • Cache: detail produk dan kategori boleh public-cache; cart, inventory, dan status order pakai no-store.
  • Security: HTTPS sebagai baseline, plus HSTS, nosniff, dan CSP.

Sekarang telusuri satu flow lengkap: user checkout produk skincare dari browser sampai response API. Tiap langkah menyentuh konsep yang sudah dibahas.

sequenceDiagram
  participant U as User (Browser)
  participant FE as React
  participant BR as Browser
  participant API as Go API
  U->>FE: klik Checkout
  FE->>BR: fetch POST /v1/checkout (Authorization, JSON)
  BR->>API: OPTIONS preflight (CORS)
  API-->>BR: 204 + header izin CORS
  BR->>API: POST /v1/checkout (Idempotency-Key)
  API->>API: cek auth, validasi cart, reserve stok
  alt cart valid
    API-->>BR: 201 Created + Location /v1/orders/1001
  else cart sudah checkout
    API-->>BR: 409 Conflict + body error
  end
  BR-->>FE: response boleh dibaca
  FE-->>U: tampilkan konfirmasi order

Gambar 10. Satu flow checkout menyentuh hampir semua konsep course ini: method, status, header, JSON, auth, CORS, dan idempotency.

Perhatikan betapa banyak konsep yang berperan dalam satu aksi tombol. Method POST karena checkout tidak idempotent. Preflight OPTIONS karena lintas origin dengan header kustom. Authorization membawa identitas pada protokol yang stateless. Status 201 dengan Location saat sukses, 409 saat konflik state. Idempotency-Key melindungi dari order ganda akibat retry.

Kamu sekarang bisa

Membaca request dan response mentah, memilih method dan status yang tepat, dan menjelaskan kenapa CORS, cache, atau auth berperilaku seperti itu.

Langkah berikutnya

Implementasi server Go: handler net/http, routing dan middleware dengan chi, lalu REST API lengkap dengan PostgreSQL, auth, dan deploy ke AWS.

🚀Dari membaca HTTP ke membangun backend

Course ini memberi peta. Langkah praktisnya: ulangi curl -v pada API favoritmu sampai tiap baris terasa familier, lalu bangun server net/http kecil yang menerapkan checklist di atas. Begitu HTTP terasa transparan, sisa perjalanan backend, dari router sampai deploy, jadi jauh lebih masuk akal.

Progress disimpan lokal di browser ini.