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.
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.
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.
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 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.
Terminalcurl -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.gopackage 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) } }
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.
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.
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 mentahPOST /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.
| Header | Arah | Fungsi |
|---|---|---|
Host | request | Nama host tujuan. Wajib di HTTP/1.1, dipakai server untuk virtual hosting. |
Accept | request | Format response yang diharapkan client, misalnya application/json. |
Content-Type | request | Format body yang dikirim client. Wajib ada bila request berisi body. |
Authorization | request | Kredensial, misalnya Bearer <token> untuk JWT. |
Content-Length | request | Panjang 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.
Terminalcurl -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.gofunc (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. }
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.
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.
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 mentahHTTP/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.
| Status | Arti singkat | Body? |
|---|---|---|
200 OK | Sukses umum, ada data dikembalikan | Ya |
201 Created | Resource baru dibuat | Biasanya ya, plus header Location |
204 No Content | Sukses tanpa data balik | Tidak |
400 Bad Request | Request rusak atau malformed | Ya, pesan error |
404 Not Found | Resource tidak ada | Ya, pesan error |
500 Internal Server Error | Bug atau kegagalan di server | Ya, 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.gofunc (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.
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.
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.
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.
| Method | Safe | Idempotent | Cacheable | Niat tipikal |
|---|---|---|---|---|
GET | Ya | Ya | Ya | Baca daftar atau detail |
HEAD | Ya | Ya | Ya | Cek keberadaan atau metadata |
OPTIONS | Ya | Ya | Tidak | Tanya kemampuan, preflight CORS |
POST | Tidak | Tidak | Hanya bila eksplisit | Buat resource, picu aksi |
PUT | Tidak | Ya | Tidak | Ganti representasi penuh |
PATCH | Tidak | Tidak | Tidak | Ubah sebagian field |
DELETE | Tidak | Ya | Tidak | Hapus resource |
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.
/v1/products Baca daftar produk, safe dan cacheable /v1/products/{id} Baca detail satu produk /v1/cart/items Tambah item ke cart, tidak idempotent /v1/cart/items/{id} Ubah qty satu item cart /v1/cart/items/{id} Hapus item dari cart, idempotent /v1/checkout Ubah cart jadi order, butuh perlindungan idempotency 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.
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.
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.
| Kode | Nama | Kapan dipakai |
|---|---|---|
200 | OK | Baca atau update sukses dengan data balik |
201 | Created | Resource baru dibuat, sertakan header Location |
204 | No Content | Sukses tanpa body, misalnya DELETE |
400 | Bad Request | Sintaks request rusak, JSON tidak valid, header wajib hilang |
401 | Unauthorized | Belum terautentikasi atau token tidak valid |
403 | Forbidden | Sudah login tetapi tidak punya hak akses |
404 | Not Found | Resource tidak ditemukan |
409 | Conflict | Konflik state, misalnya slug produk sudah dipakai |
422 | Unprocessable Content | Sintaks benar tetapi gagal validasi semantik atau aturan bisnis |
429 | Too Many Requests | Rate limit terlampaui, biasanya dengan Retry-After |
500 | Internal Server Error | Bug 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).
- 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.
- Server paham payload, tetapi nilainya melanggar aturan bisnis.
qty: 0, harga negatif,emailtidak 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.
| Area | Sukses | Error tipikal |
|---|---|---|
| Login | 200 + token | 401 kredensial salah, 429 brute force |
| Detail produk | 200 | 404 produk tidak ada |
| Tambah ke cart | 201 atau 200 | 422 qty invalid, 404 produk tidak ada |
| Checkout | 201 order | 409 cart sudah checkout, 422 stok kurang |
Di Go, status ditulis lewat konstanta http.Status... agar terbaca jelas, bukan angka mentah.
internal/cart/handler.gofunc (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) }
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.
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.
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.
| Header | Peran | Catatan |
|---|---|---|
Content-Type | Format body | Wajib bila ada body, misalnya application/json |
Accept | Format response yang diminta client | Dasar content negotiation |
Authorization | Kredensial | Bearer <token>, jangan pernah di-log mentah |
Cache-Control | Aturan cache | public versus private, max-age, no-store |
ETag | Sidik jari versi resource | Dasar conditional request dan 304 |
Location | Alamat resource baru atau tujuan redirect | Dipakai pada 201 dan 3xx |
Set-Cookie | Server menanam cookie di client | Atur HttpOnly, Secure, SameSite |
X-Request-ID | Penanda unik per request | Untuk 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.
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 loginHTTP/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.gofunc (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) }
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.
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.
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.gotype 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) }
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.
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.
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.
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.
GET /v1/products/42menunjuk satu produk spesifik.GET /v1/orders/1001menunjuk satu order.- Menghapus id mengubah arti URL sepenuhnya.
?skin_type=oilymenyaring berdasarkan jenis kulit.?page=2&per_page=20mengatur pagination.?sort=price_ascmengatur 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.
/v1/products/{id} Identitas: detail satu produk lewat path param /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.gofunc (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) }
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.
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.
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.gofunc 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) }) } }
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.
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.
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.
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.
| Directive | Arti | Cocok untuk |
|---|---|---|
public, max-age=3600 | Boleh di-cache siapa saja selama 1 jam | Daftar kategori, detail produk |
private, max-age=60 | Hanya cache browser, 1 menit | Data per-user yang jarang berubah |
no-store | Tidak boleh disimpan di mana pun | Cart, 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
endGambar 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.gofunc (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.
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.
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.
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.
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.
| Header | Fungsi |
|---|---|
Strict-Transport-Security | HSTS: paksa browser selalu memakai HTTPS untuk domain ini, dengan max-age |
X-Content-Type-Options: nosniff | Larang browser menebak MIME, mencegah eksekusi response sebagai script |
Content-Security-Policy | Batasi 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.gofunc 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) }) }
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.
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.
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.
/v1/auth/login Login, balas token, 200 atau 401 /v1/products Daftar produk dengan filter dan pagination, public-cacheable /v1/products/{id} Detail produk, ETag dan Cache-Control /v1/cart Isi cart milik user, no-store /v1/cart/items Tambah item ke cart, 201 atau 422 /v1/cart/items/{id} Ubah qty item cart /v1/cart/items/{id} Hapus item dari cart, 204 /v1/checkout Ubah cart jadi order, 201 atau 409, butuh idempotency /v1/orders Riwayat order user, no-store /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 frontend | Endpoint | Catatan HTTP |
|---|---|---|
| Daftar produk | GET /v1/products?skin_type=oily&page=1 | 200, public cache, query filter |
| Detail produk | GET /v1/products/{id} | 200 atau 404, ETag |
| Cart | GET /v1/cart, POST /v1/cart/items | no-store, butuh auth |
| Checkout | POST /v1/checkout | 201 atau 409, idempotency key |
| Riwayat order | GET /v1/orders | no-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.gopackage 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.gopackage 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.
- 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.
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.
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.
| Versi | Transport | Ciri utama |
|---|---|---|
| HTTP/1.1 | TCP | Connection reuse (keep-alive), request relatif berurutan |
| HTTP/2 | TCP | Multiplexing satu koneksi, masih kena HOL blocking di TCP |
| HTTP/3 | QUIC (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.
Terminalcurl --http2 -I https://example.com # Baris pertama menunjukkan versi yang dipakai, mis. HTTP/2 200.
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.gofunc 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")) }) }
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.
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.
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 orderGambar 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.
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.