L
REST API Design • Literally Web ID

Desain RESTful API Skalabel dengan Rate Limiting dan JWT Authentication di Go

⏱️ 15 menit baca • 📅 20 Oct 2026 • 👁️ 2 views
Desain RESTful API Skalabel dengan Rate Limiting dan JWT Authentication di Go

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan aplikasi web modern, arsitektur backend yang andal bukan lagi sekadar kemewahan, melainkan fondasi mutlak bagi kelangsungan bisnis digital. Ketika sebuah sistem API publik mulai mengalami peningkatan traffic yang signifikan, tantangan struktural langsung bermunculan: mulai dari kerentanan terhadap serangan brute force dan credential stuffing, kelelahan sumber daya server akibat lonjakan permintaan (denial of service secara tidak sengaja maupun terarah), hingga kekacauan struktur basis kode (spaghetti code) yang membuat pemeliharaan menjadi mimpi buruk bagi para insinyur.

Secara historis, banyak pengembang membangun API di Go dengan langsung mencampur logika penanganan HTTP, aturan bisnis, dan akses basis kode penyimpanan data ke dalam satu fungsi penanganan tunggal (http.HandleFunc). Pendekatan naif ini mungkin berfungsi dengan baik pada fase prototipe (Minimum Viable Product), namun langsung runtuh di bawah tekanan lingkungan produksi nyata. Masalah utamanya meliputi:

  1. Keterikatan Kuat (Tight Coupling): Logika bisnis terikat erat dengan framework HTTP atau driver basis data, membuat unit testing menjadi sangat sulit dan lambat.
  2. Manajemen Status Otentikasi yang Buruk: Penggunaan mekanisme stateful berbasis sesi (session-based) lama yang membebani memori server atau penggunaan token JWT statis tanpa mekanisme rotasi (token rotation) yang memadai, membuka celah keamanan dari pembajakan token (token hijacking).
  3. Ketiadaan Kontrol Lalu Lintas Terdistribusi: Tanpa sistem pembatasan laju (rate limiting) yang terdistribusi secara efisien, sebuah endpoint otentikasi dapat dengan mudah dilumpuhkan oleh botnet yang mengirimkan jutaan permintaan per detik, melumpuhkan seluruh instans server.

Perbandingan Kondisi: Sebelum vs Sesudah Penerapan Arsitektur Skalabel

Parameter Sebelum (Naif / Monolitik Tanpa Proteksi) Sesudah (Clean Arch, JWT Rotation & Redis Rate Limit)
Latensi Puncak (p99) > 850 ms di bawah beban 5.000 req/s < 45 ms dengan pemanfaatan caching Redis
Keamanan Token JWT statis dengan masa berlaku 7 hari; tidak bisa dicabut sebelum kedaluwarsa JWT akses pendek (15 menit) + Refresh Token rotasi di basis data/Redis
Ketahanan Beban Server langsung mengalami Out of Memory (OOM) atau Goroutine leak saat traffic melonjak Permintaan berlebih ditolak secara elegan di lapisan Edge/Middleware dengan HTTP 429
Skalabilitas & Testing Mocking basis data hampir mustahil; perubahan skema merusak seluruh pengendali HTTP Lapisan domain murni; mocking repository sangat mudah, pengujian unit mencapai >90% coverage

2. Konsep Inti & Deep Dive Arsitektur

Untuk mengatasi tantangan di atas, kita harus mengimplementasikan arsitektur berlapis yang memisahkan tanggung jawab secara ketat, dipadukan dengan mekanisme keamanan kriptografi dan manajemen antrean penyimpanan sementara.

Clean Architecture (Hexagonal / Onion Variant) di Go

Prinsip utama Clean Architecture adalah Dependency Inversion Principle. Lapisan luar (infrastruktur, kerangka kerja web, pengendali HTTP, dan driver basis data) bergantung pada lapisan dalam (domain dan kasus penggunaan), tetapi lapisan dalam sama sekali tidak mengetahui detail tentang lapisan luar.

+-------------------------------------------------------------+
|                    Frameworks & Drivers                     | 
|         (Go Fiber/Chi, PostgreSQL, Redis, Fiber HTTP)       | 
|  +-------------------------------------------------------+  | 
|  |                  Interface Adapters                   |  | 
|  |            (Controllers, Presenters, Gateways)        |  | 
|  |  +-------------------------------------------------+  |  | 
|  |  |                Use Cases                        |  |  | 
|  |  |        (Application Business Logic)             |  |  | 
|  |  |  +-------------------------------------------+  |  |  | 
|  |  |  |                Entities                   |  |  |  | 
|  |  |  |        (Enterprise Business Rules)        |  |  |  | 
|  |  |  +-------------------------------------------+  |  |  | 
|  |  +-------------------------------------------------+  |  | 
|  +-------------------------------------------------------+  | 
+-------------------------------------------------------------+

Alur Eksekusi Permintaan (Request Lifecycle)

  1. Klien mengirimkan HTTP Request yang membawa Header Authorization: Bearer <JWT_ACCESS_TOKEN>.
  2. Nginx Reverse Proxy menerima permintaan, melakukan SSL Termination, dan meneruskannya ke aplikasi Go pada port internal.
  3. Distributed Rate Limiter Middleware (Redis) memeriksa token atau alamat IP klien menggunakan algoritma Token Bucket. Jika melebihi kuota, langsung kembalikan HTTP 429 Too Many Requests.
  4. JWT Authentication Middleware memvalidasi tanda tangan kriptografi (HMAC-SHA256 atau RSA) dari token akses, mengekstrak klaim (claims), dan menyuntikkannya ke dalam konteks permintaan (context.Context).
  5. Handler / Controller menerima konteks yang telah disterilkan, meneruskannya ke Use Case, yang kemudian memanggil Repository untuk berinteraksi dengan Database (PostgreSQL).
  6. Respon dikembalikan melalui rantai lapisan yang sama dalam bentuk JSON terstruktur.

3. Perbandingan Komparatif / Analisis Pendekatan

Dalam merancang sistem ini, terdapat beberapa pilihan pendekatan teknologi yang perlu dievaluasi berdasarkan trade-off sumber daya:

Komponen Pendekatan A (Pilihan Kita) Pendekatan B (Alternatif Umum) Analisis Trade-off & Keputusan
Rate Limiting Store Redis (In-Memory Datastore) Relational DB (PostgreSQL Table) Redis menawarkan kecepatan in-memory ($O(1)$ time complexity dengan Lua script), mencegah beban tulis berlebih pada database utama.
HTTP Router/Framework go-chi/chi (Mux ringan, standar net/http) gin-gonic/gin atau gofiber/fiber Chi mempertahankan kompatibilitas 100% dengan net/http standar Go, sangat modular, tidak menggunakan unsafe pointer seperti Fiber, dan minim abstraksi berlebih.
JWT Validation Golang-jwt v5 dengan HMAC/RSA PASETO (Platform-Agnostic Security Tokens) JWT dipilih karena standar industri yang matang, interoperabilitas luas dengan klien frontend, sementara PASETO meski lebih aman dari sisi kriptografi, masih memiliki adopsi ekosistem yang terbatas di beberapa SDK klien.

4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum mulai menulis kode, pastikan lingkungan pengembangan Anda memenuhi spesifikasi berikut:

  • Go Runtime: Versi 1.22 atau lebih baru.
  • Redis Server: Versi 7.x (mendukung eksekusi skrip Lua atomik).
  • PostgreSQL: Versi 15+
  • OS: Linux (Ubuntu 22.04 LTS / Debian 12) atau macOS Sonoma untuk pengembangan lokal.

Inisialisasi Proyek

Jalankan perintah terminal berikut untuk menyiapkan struktur direktori sesuai Clean Architecture:

mkdir -p go-secure-api/{cmd/api,internal/domain,internal/usecase,internal/delivery/http,internal/infrastructure/persistence,internal/infrastructure/cache,pkg/jwt}
cd go-secure-api
go mod init github.com/username/go-secure-api

Instal dependensi esensial:

go get github.com/go-chi/chi/v5
go get github.com/golang-jwt/jwt/v5
go get github.com/redis/go-redis/v9
go get github.com/lib/pq
go get golang.org/x/crypto/bcrypt
go get github.com/joho/godotenv

5. Panduan Implementasi Langkah demi Langkah

A. Domain & Entitas (internal/domain/user.go)

Mendefinisikan struktur inti dan kontrak antarmuka (interface) tanpa ketergantungan luar.

package domain

import (
	"context"
	"time"
)

type User struct {
	ID        int64     `json:"id"`
	Email     string    `json:"email"`
	Password  string    `json:"-"`
	CreatedAt time.Time `json:"created_at"`
}

type TokenPair struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
}

type UserRepository interface {
	Create(ctx context.Context, user *User) error
	FindByEmail(ctx context.Context, email string) (*User, error)
}

type TokenRepository interface {
	StoreRefreshToken(ctx context.Context, userID int64, token string, expiresAt time.Duration) error
	GetRefreshToken(ctx context.Context, userID int64) (string, error)
	RevokeRefreshToken(ctx context.Context, userID int64) error
}

B. Implementasi JWT & Keamanan Token (pkg/jwt/jwt.go)

Menangani pembuatan token akses berumur pendek dan token penyegaran berumur panjang.

package jwt

import (
	"errors"
	"fmt"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

type Claims struct {
	UserID int64 `json:"user_id"`
	jwt.RegisteredClaims
}

type JWTManager struct {
	secretKey     string
	accessTokenDuration  time.Duration
	refreshTokenDuration time.Duration
}

func NewJWTManager(secretKey string, accessDur, refreshDur time.Duration) *JWTManager {
	return &JWTManager{
		secretKey:            secretKey,
		accessTokenDuration:  accessDur,
		refreshTokenDuration: refreshDur,
	}
}

func (m *JWTManager) GenerateTokenPair(userID int64) (string, string, error) {
	// Generate Access Token
	accessClaims := &Claims{
		UserID: userID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.accessTokenDuration)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
		},
	}
	accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString([]byte(m.secretKey))
	if err != nil {
		return "", "", err
	}

	// Generate Refresh Token
	refreshClaims := &Claims{
		UserID: userID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.refreshTokenDuration)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
		},
	}
	refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString([]byte(m.secretKey))
	if err != nil {
		return "", "", err
	}

	return accessToken, refreshToken, nil
}

func (m *JWTManager) VerifyToken(tokenStr string) (*Claims, error) {
	token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		_, ok := token.Method.(*jwt.SigningMethodHMAC)
		if !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(m.secretKey), nil
	})

	if err != nil {
		return nil, err
	}

	claims, ok := token.Claims.(*Claims)
	if !ok || !token.Valid {
		return nil, errors.New("invalid token claims")
	}

	return claims, nil
}

C. Distributed Rate Limiter Menggunakan Redis & Lua Script (internal/infrastructure/cache/rate_limiter.go)

[!IMPORTANT] Penggunaan skrip Lua pada Redis memastikan operasi pengecekan dan penambahan penghitung (counter) berjalan secara atomik (atomic execution), menghindari race condition pada lingkungan terdistribusi berskala tinggi.

package cache

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

type RateLimiter struct {
	client *redis.Client
	limit  int
	window time.Duration
}

func NewRateLimiter(client *redis.Client, limit int, window time.Duration) *RateLimiter {
	return &RateLimiter{
		client: client,
		limit:  limit,
		window: window,
	}
}

var luaScript = redis.NewScript(`
	local key = KEYS[1]
	local limit = tonumber(ARGV[1])
	local window = tonumber(ARGV[2])

	local current = redis.get(key)
	if current and tonumber(current) >= limit then
		return 0
	end

	local res = redis.call("INCR", key)
	if res == 1 then
		redis.call("EXPIRE", key, window)
	end
	return 1
`)

func (r *RateLimiter) Allow(ctx context.Context, identifier string) (bool, error) {
	key := fmt.Sprintf("ratelimit:%s", identifier)
	result, err := r.client.Eval(ctx, luaScript.Hash(), []string{key}, r.limit, int(r.window.Seconds())).Result()
	if err != nil {
		// Fallback: Jika Redis down, izinkan request agar ketersediaan sistem tetap terjaga (Fail-Open)
		return true, err
	}

	return result == int64(1), nil
}

D. HTTP Middleware & Router (internal/delivery/http/middleware.go & router.go)

package http

import (
	"context" 
	"net/http"
	"strings"

	"github.com/username/go-secure-api/internal/infrastructure/cache"
	"github.com/username/go-secure-api/pkg/jwt"
)

func AuthMiddleware(jwtManager *jwt.JWTManager) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			authHeader := r.Header.Get("Authorization")
			if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
				http.Error(w, `{"error": "missing or malformed authorization header\
🏷️ Tags: #Nginx #High Concurrency #Performance #Sysadmin #Web Server