Belajar Golang dari Nol #13: Middleware dan Auth Sederhana
Di Belajar Golang dari Nol #12 kita sudah menyambungkan API produk ke database. Data sekarang tersimpan beneran, bukan hilang tiap server restart. Tapi ada satu masalah besar yang belum kita sentuh. API kita masih terbuka lebar. Siapa pun yang tahu alamatnya bisa POST produk baru, mengubah harga, bahkan menghapus data. Di bagian ini kita tutup lubang itu pakai dua konsep: middleware dan autentikasi sederhana.
Masalahnya: API kita masih pintu tanpa kunci
Coba bayangkan API produk dari bagian 12 sudah kamu deploy ke server. Alamatnya ketahuan orang. Apa yang terjadi?
- Orang iseng bisa kirim
POST /produkdan mengisi database kamu dengan data sampah. - Kompetitor bisa hapus semua produk lewat
DELETE. - Kamu tidak tahu siapa yang mengakses, kapan, dan endpoint mana yang paling sering dipanggil.
Jadi ada dua kebutuhan yang berbeda tapi berkaitan. Pertama, kita perlu tahu siapa pemanggil API dan menolak yang tidak berhak. Itu namanya autentikasi. Kedua, pengecekan ini harus jalan di banyak endpoint sekaligus. Kalau kita tulis kode cek di setiap handler satu per satu, itu copy paste yang bakal jadi mimpi buruk saat endpoint bertambah. Solusi untuk masalah kedua ini namanya middleware.
Apa itu middleware
Middleware adalah function yang membungkus handler. Dia berdiri di antara request masuk dan handler yang memprosesnya.
Analoginya begini. Bayangkan gedung kantor dengan banyak ruangan. Setiap ruangan adalah handler: ruang produk, ruang laporan, ruang admin. Middleware adalah satpam di lobi. Semua tamu harus lewat dia dulu, mau ke ruangan mana pun. Satpam bisa mencatat tamu di buku (logging), memeriksa kartu identitas (auth), atau menolak tamu yang mencurigakan. Ruangan tidak perlu tahu urusan pemeriksaan itu. Mereka cukup fokus pada pekerjaannya.
Bentuk middleware di Go
Di Go, middleware punya bentuk yang khas:
func namaMiddleware(next http.Handler) http.Handler
Baca pelan. Function ini menerima sebuah http.Handler dan mengembalikan http.Handler juga. Masih ingat dua materi lama kita? http.Handler adalah interface, yaitu apa pun yang punya method ServeHTTP. Dan function di Go adalah nilai yang bisa dioper dan dikembalikan seperti angka atau string. Middleware menggabungkan keduanya. Dia menerima handler asli lewat parameter next, lalu mengembalikan handler baru yang menjalankan logika tambahan sebelum atau sesudah memanggil next.
Karena input dan output sama-sama http.Handler, hasil bungkusan bisa dibungkus lagi. Ini yang bikin middleware bisa dirangkai.
Middleware pertama: logging
Kita mulai dari yang paling tidak berbahaya: mencatat setiap request. Method apa, path apa, dan berapa lama diproses.
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mulai := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s selesai dalam %v", r.Method, r.URL.Path, time.Since(mulai))
})
}
Perhatikan alurnya. Kita catat waktu mulai, panggil handler asli lewat next.ServeHTTP(w, r), lalu hitung durasinya pakai time.Since. Kode sebelum next.ServeHTTP jalan sebelum handler. Kode sesudahnya jalan setelah handler selesai.
http.HandlerFunc di sini adalah adapter yang sudah kita kenal sejak bagian 11. Dia mengubah function biasa menjadi sesuatu yang memenuhi interface http.Handler.
Cara pasangnya: bungkus mux sebelum diserahkan ke server.
mux := http.NewServeMux()
mux.HandleFunc("GET /produk", listProduk)
log.Fatal(http.ListenAndServe(":8080", logging(mux)))
Karena yang dibungkus adalah mux, semua endpoint otomatis tercatat. Satu function, berlaku di mana-mana. Tidak ada copy paste.
Middleware kedua: API key
Sekarang bagian auth. Cara paling sederhana adalah API key: sebuah string rahasia yang harus dikirim client lewat header. Kalau key cocok, request lolos. Kalau tidak, tolak dengan status 401 Unauthorized.
func cekAPIKey(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
kunci := os.Getenv("API_KEY")
if kunci == "" || r.Header.Get("X-API-Key") != kunci {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "API key salah atau tidak ada"})
return
}
next.ServeHTTP(w, r)
})
}
Ada beberapa hal penting di sini.
Pertama, key aslinya diambil dari environment variable lewat os.Getenv, bukan ditulis langsung di kode. Kenapa? Karena kode biasanya masuk Git. Kalau key kamu hardcode, siapa pun yang bisa baca repo tahu rahasianya. Dengan environment variable, key hidup di server, terpisah dari kode. Ganti key pun tidak perlu compile ulang.
Kedua, kita cek kunci == "" dulu. Kalau server lupa diset API_KEY, semua request ditolak. Ini lebih aman daripada sebaliknya, yaitu semua request lolos karena membandingkan string kosong dengan string kosong.
Ketiga, jangan lupa return setelah menulis respons 401. Tanpa itu, eksekusi lanjut ke next.ServeHTTP dan handler tetap jalan. Ini bug klasik middleware.
Satu catatan jujur. API key statis seperti ini cocok untuk API internal atau komunikasi antar sistem, misalnya server kamu dipanggil oleh server kantor lain yang sudah kamu kenal. Untuk login user publik dengan ribuan akun, ini tidak cukup. Kita bahas alasannya di bawah.
Merangkai beberapa middleware
Karena middleware menerima handler dan mengembalikan handler, kita bisa menumpuknya:
handler := logging(cekAPIKey(mux))
log.Fatal(http.ListenAndServe(":8080", handler))
Urutan bacanya dari luar ke dalam. Request masuk ke logging dulu, lalu cekAPIKey, baru sampai ke mux dan handler. Respons berjalan sebaliknya, dari dalam ke luar.
Cara paling gampang membuktikan urutan adalah menambah log sementara di tiap middleware. Kalau kamu tambahkan log.Println("masuk logging") di awal logging dan log.Println("masuk cekAPIKey") di awal cekAPIKey, outputnya seperti ini:
2026/07/26 10:15:02 masuk logging
2026/07/26 10:15:02 masuk cekAPIKey
2026/07/26 10:15:02 POST /produk selesai dalam 3.1ms
Terlihat jelas. Middleware terluar jalan duluan. Baris durasi muncul terakhir karena dia ditulis setelah next.ServeHTTP selesai. Urutan ini penting saat middleware saling bergantung. Logging biasanya paling luar supaya request yang ditolak auth pun tetap tercatat.
Middleware selektif: tidak semua endpoint butuh kunci
Ada masalah baru. Kalau cekAPIKey membungkus seluruh mux, endpoint GET /produk ikut terkunci. Padahal daftar produk biasanya memang untuk publik. Yang perlu dijaga hanya operasi tulis: POST, PUT, DELETE.
Solusinya, pasang middleware per route, bukan di mux. mux.Handle menerima http.Handler, jadi kita bisa membungkus handler tertentu saja:
mux := http.NewServeMux()
// publik, tanpa kunci
mux.HandleFunc("GET /produk", listProduk)
mux.HandleFunc("GET /produk/{id}", detailProduk)
// dilindungi API key
mux.Handle("POST /produk", cekAPIKey(http.HandlerFunc(tambahProduk)))
mux.Handle("PUT /produk/{id}", cekAPIKey(http.HandlerFunc(ubahProduk)))
mux.Handle("DELETE /produk/{id}", cekAPIKey(http.HandlerFunc(hapusProduk)))
// logging tetap membungkus semua
handler := logging(mux)
Pola "POST /produk" dengan method di depan adalah fitur routing Go 1.22 yang sudah kita pakai sejak bagian 11. Sekarang terasa manfaatnya: satu path bisa punya perlakuan beda per method.
Sekilas jujur soal auth user sungguhan
API key cukup untuk API internal. Tapi begitu kamu bikin aplikasi dengan user yang mendaftar dan login sendiri, kebutuhannya beda jauh. Ada tiga hal yang minimal harus kamu tahu namanya.
Pertama, password tidak boleh disimpan mentah di database. Password harus di-hash pakai algoritma yang memang dirancang untuk itu, misalnya bcrypt. Hash itu satu arah. Kalau database bocor, penyerang tidak langsung dapat password asli.
Kedua, setelah user login, server perlu mengingat dia di request berikutnya. Ada dua pendekatan umum. Session: server menyimpan data login dan memberi user sebuah ID lewat cookie. Token JWT: server memberi user token berisi data yang sudah ditandatangani, dan server tinggal memverifikasi tanda tangannya tanpa menyimpan apa pun.
Ketiga, masing-masing pendekatan punya konsekuensi soal logout, expiry, dan keamanan yang tidak bisa dijelaskan dua paragraf. Jadi kita tidak implement sekarang. Topik ini butuh bagian sendiri, dan enaknya dibahas setelah kita bisa menulis test. Kode auth tanpa test itu ngeri.
Bonus singkat: CORS
Satu lagi middleware yang cepat atau lambat kamu butuhkan. Kalau nanti API ini dipanggil dari frontend JavaScript yang berjalan di domain lain, misalnya localhost:3000 memanggil localhost:8080, browser akan memblokir responsnya. Ini kebijakan keamanan browser bernama same-origin policy. Supaya diizinkan, server harus mengirim header CORS yang menyatakan siapa boleh mengakses.
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
Tanda * berarti semua domain boleh. Untuk produksi, ganti dengan domain frontend kamu. Detail CORS panjang, tapi bentuk middleware-nya sama saja dengan yang sudah kita pelajari.
Program utuh
Sekarang kita gabungkan semuanya dengan API produk dari bagian 12. Struktur handler tidak berubah, kita hanya menambah lapisan di depannya.
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
)
type Produk struct {
ID int `json:"id"`
Nama string `json:"nama"`
Harga int `json:"harga"`
}
var db *sql.DB
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mulai := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s selesai dalam %v", r.Method, r.URL.Path, time.Since(mulai))
})
}
func cekAPIKey(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
kunci := os.Getenv("API_KEY")
if kunci == "" || r.Header.Get("X-API-Key") != kunci {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "API key salah atau tidak ada"})
return
}
next.ServeHTTP(w, r)
})
}
func listProduk(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id, nama, harga FROM produk")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
produk := []Produk{}
for rows.Next() {
var p Produk
if err := rows.Scan(&p.ID, &p.Nama, &p.Harga); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
produk = append(produk, p)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(produk)
}
func tambahProduk(w http.ResponseWriter, r *http.Request) {
var p Produk
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "body tidak valid", http.StatusBadRequest)
return
}
hasil, err := db.Exec("INSERT INTO produk (nama, harga) VALUES (?, ?)", p.Nama, p.Harga)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
id, _ := hasil.LastInsertId()
p.ID = int(id)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(p)
}
func main() {
var err error
db, err = sql.Open("mysql", os.Getenv("DSN"))
if err != nil {
log.Fatal(err)
}
if err := db.Ping(); err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("GET /produk", listProduk)
mux.Handle("POST /produk", cekAPIKey(http.HandlerFunc(tambahProduk)))
log.Println("Server jalan di :8080")
log.Fatal(http.ListenAndServe(":8080", logging(mux)))
}
Jalankan dengan environment variable yang lengkap:
export DSN="root:passwordmu@tcp(127.0.0.1:3306)/belajar_golang"
export API_KEY="rahasia-123"
go run main.go
Uji dengan curl
Coba POST tanpa API key dulu:
curl -i -X POST http://localhost:8080/produk \
-H "Content-Type: application/json" \
-d '{"nama":"Kopi Arabika","harga":85000}'
Responsnya ditolak:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{"error":"API key salah atau tidak ada"}
Sekarang ulangi dengan header X-API-Key:
curl -i -X POST http://localhost:8080/produk \
-H "Content-Type: application/json" \
-H "X-API-Key: rahasia-123" \
-d '{"nama":"Kopi Arabika","harga":85000}'
Kali ini berhasil:
HTTP/1.1 201 Created
Content-Type: application/json
{"id":4,"nama":"Kopi Arabika","harga":85000}
Sementara itu GET /produk tetap bisa diakses siapa saja tanpa header apa pun. Dan di terminal server, setiap request tercatat rapi oleh middleware logging.
Penutup
API produk kita sekarang punya satpam. Logging mencatat semua tamu, API key menjaga pintu operasi tulis, dan tidak ada satu pun kode pengecekan yang di-copy paste antar handler. Kamu juga sudah paham kenapa pola func(http.Handler) http.Handler bisa dirangkai, dan kapan API key cukup serta kapan butuh auth yang lebih serius.
Tapi jujur saja, dari bagian 11 sampai sekarang kita mengetes API cuma lewat curl dan mata. Begitu kode makin panjang, cara itu tidak bisa diandalkan. Di bagian 14 kita bahas “Testing di Go: Menguji Function dan Handler API” supaya setiap perubahan bisa diverifikasi otomatis.
Kalau kamu sedang membangun API atau sistem internal untuk bisnis dan butuh bantuan yang serius, tim kami di Arrazy Inovasi menyediakan jasa pembuatan sistem aplikasi dari perancangan sampai deployment.
Artikel Lainnya di Kategori Golang
Golang 31 Juli 2026
Belajar Golang dari Nol #5: Slice dan Map, Kumpulan Data di Go
Lanjutan seri Go pemula: slice, append, map, comma ok idiom, jebakan nil map, plus latihan keranjang belanja mini dengan pengecekan stok.
Baca Artikel
Golang 27 Juli 2026
Belajar Golang dari Nol #1: Kenalan dengan Go dan Cara Installnya
Kenalan dengan Go: bahasa apa, siapa pemakainya, cara installnya di Linux, Windows, macOS, sampai program pertama. Panduan pemula tanpa ribet.
Baca Artikel
Golang 2 Agustus 2026
Belajar Golang dari Nol #7: Pointer, Alamat Memori Tanpa Pusing
Pahami pointer Go lewat analogi fotokopi vs alamat rumah. Operator & dan *, pointer receiver, nil pointer, sampai latihan sistem saldo rekening.
Baca ArtikelIngin Membaca Artikel Lainnya?
Temukan lebih banyak insight dan tips tentang teknologi dan bisnis digital.
Lihat Semua Artikel