Middleware Gin dari A sampai Z: Bikin Middleware Custom Sendiri, Yuk!

Pendahuluan

Hampir setiap aplikasi backend punya kebutuhan yang sama di banyak endpoint: mengecek autentikasi, mencatat log request, membatasi rate limit, menangani panic, atau menambahkan header tertentu ke response. Kalau logic ini ditulis manual di setiap handler, kode akan penuh duplikasi dan sulit dirawat. Di sinilah middleware berperan — kode yang dijalankan sebelum (atau sesudah) handler utama, tanpa perlu mengubah handler itu sendiri.

Artikel ini membahas cara kerja middleware di Gin, middleware bawaan yang sering dipakai, hingga cara membuat middleware custom sendiri dari nol.

Apa Itu Middleware?

Middleware adalah fungsi yang “menyisip” di antara request masuk dan handler yang menanganinya. Middleware bisa:

  • Memodifikasi request sebelum diteruskan ke handler (misalnya menambahkan data user ke context).
  • Menghentikan request lebih awal jika suatu kondisi tidak terpenuhi (misalnya token tidak valid).
  • Menjalankan logic tambahan setelah handler selesai (misalnya logging waktu eksekusi).

Secara konsep, middleware di Gin membentuk semacam “rantai” (chain): request akan melewati middleware satu per satu sebelum akhirnya sampai ke handler.

Request → Middleware A → Middleware B → Handler → Response

Cara Kerja Middleware di Gin

Middleware di Gin berbentuk fungsi dengan tipe gin.HandlerFunc:

go

func(c *gin.Context)

Kuncinya ada pada fungsi c.Next() dan c.Abort():

  • c.Next() — melanjutkan eksekusi ke middleware/handler berikutnya dalam rantai. Kode setelah c.Next() akan dijalankan setelah handler selesai diproses (mirip konsep “after” dalam middleware pattern).
  • c.Abort() — menghentikan rantai eksekusi, sehingga middleware/handler setelahnya tidak akan dijalankan.

Contoh middleware paling sederhana:

go

func LoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()

        c.Next() // lanjut ke handler berikutnya

        duration := time.Since(start)
        log.Printf("[%s] %s - %v", c.Request.Method, c.Request.URL.Path, duration)
    }
}

Middleware ini mencatat waktu sebelum request diproses (start), memanggil c.Next() agar handler selanjutnya dijalankan, lalu menghitung durasi setelah handler selesai.

Mendaftarkan Middleware

Middleware di Gin bisa didaftarkan di beberapa level:

1. Global (berlaku untuk semua route)

go

router := gin.New()
router.Use(LoggerMiddleware())
router.Use(gin.Recovery())

2. Per Group

go

adminRoutes := router.Group("/admin")
adminRoutes.Use(AuthRequired(), RequireRole("admin"))
{
    adminRoutes.GET("/dashboard", getDashboard)
}

3. Per Route (spesifik satu endpoint)

go

router.DELETE("/users/:id", AuthRequired(), deleteUser)

Gin mengizinkan lebih dari satu middleware sekaligus dipasang di satu route, dan urutan pendaftarannya menentukan urutan eksekusi.

Middleware Bawaan Gin

Gin sudah menyediakan beberapa middleware siap pakai:

  • gin.Logger() — mencatat log setiap request (method, path, status code, durasi).
  • gin.Recovery() — menangkap panic agar aplikasi tidak crash, dan mengembalikan response 500.
  • gin.BasicAuth() — autentikasi sederhana berbasis username/password.

go

router := gin.New() // tanpa middleware default
router.Use(gin.Logger())
router.Use(gin.Recovery())

Catatan: gin.Default() sebenarnya sudah otomatis memasang Logger() dan Recovery(), sedangkan gin.New() memberi kanvas kosong tanpa middleware apa pun — cocok dipakai saat ingin mengganti keduanya dengan versi custom.

Membuat Middleware Custom

1. Middleware Autentikasi JWT

Salah satu middleware yang hampir selalu dibutuhkan adalah autentikasi berbasis token:

go

func AuthRequired() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"message": "Token tidak ditemukan"})
            c.Abort()
            return
        }

        tokenString := strings.TrimPrefix(authHeader, "Bearer ")
        claims, err := jwt.ValidateToken(tokenString)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"message": "Token tidak valid"})
            c.Abort()
            return
        }

        // Simpan data user ke context agar bisa dipakai handler berikutnya
        c.Set("user_id", claims.UserID)
        c.Set("role", claims.Role)

        c.Next()
    }
}

Perhatikan penggunaan c.Set() untuk menyimpan data ke dalam context request, yang nantinya bisa diambil kembali di handler:

go

func GetProfile(c *gin.Context) {
    userID, _ := c.Get("user_id")
    c.JSON(200, gin.H{"user_id": userID})
}

2. Middleware Role-Based Access Control (RBAC)

Middleware bisa juga dibuat lebih fleksibel dengan menerima parameter, misalnya untuk mengecek role tertentu:

go

func RequireRole(allowedRoles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        role, exists := c.Get("role")
        if !exists {
            c.JSON(http.StatusForbidden, gin.H{"message": "Role tidak ditemukan"})
            c.Abort()
            return
        }

        for _, allowed := range allowedRoles {
            if role == allowed {
                c.Next()
                return
            }
        }

        c.JSON(http.StatusForbidden, gin.H{"message": "Akses ditolak"})
        c.Abort()
    }
}

Penggunaannya:

go

adminRoutes.Use(AuthRequired(), RequireRole("admin", "superadmin"))

3. Middleware Recovery Custom

Seperti dibahas di artikel sebelumnya soal standard response format, middleware recovery custom berguna agar panic tetap menghasilkan response yang konsisten:

go

func CustomRecovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic recovered: %v", err)
                c.JSON(http.StatusInternalServerError, gin.H{
                    "success": false,
                    "message": "Terjadi kesalahan pada server",
                })
                c.Abort()
            }
        }()
        c.Next()
    }
}

4. Middleware Rate Limiting Sederhana

Middleware juga cocok dipakai untuk membatasi jumlah request dari satu client dalam periode tertentu, misalnya menggunakan package golang.org/x/time/rate:

go

var limiters = make(map[string]*rate.Limiter)
var mu sync.Mutex

func getLimiter(ip string) *rate.Limiter {
    mu.Lock()
    defer mu.Unlock()

    limiter, exists := limiters[ip]
    if !exists {
        limiter = rate.NewLimiter(1, 5) // 1 request/detik, burst 5
        limiters[ip] = limiter
    }
    return limiter
}

func RateLimitMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        ip := c.ClientIP()
        limiter := getLimiter(ip)

        if !limiter.Allow() {
            c.JSON(http.StatusTooManyRequests, gin.H{"message": "Terlalu banyak request"})
            c.Abort()
            return
        }

        c.Next()
    }
}

5. Middleware Request ID

Berguna untuk tracing/debugging, terutama di sistem yang sudah terdistribusi ke beberapa service:

go

func RequestIDMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        requestID := uuid.New().String()
        c.Set("request_id", requestID)
        c.Header("X-Request-ID", requestID)
        c.Next()
    }
}

Urutan Middleware Itu Penting

Urutan pendaftaran middleware menentukan urutan eksekusinya. Contoh urutan yang umum dan disarankan:

go

router.Use(RequestIDMiddleware())   // 1. beri ID ke setiap request
router.Use(CustomRecovery())        // 2. tangkap panic sedini mungkin
router.Use(LoggerMiddleware())      // 3. catat log request
router.Use(cors.Default())          // 4. tangani CORS sebelum ke business logic

protected := router.Group("/api/v1")
protected.Use(AuthRequired())       // 5. baru cek autentikasi di route yang butuh

Middleware seperti recovery sebaiknya dipasang paling awal, karena tujuannya menangkap error dari middleware maupun handler yang dijalankan setelahnya.

Kesalahan Umum saat Membuat Middleware

  1. Lupa memanggil c.Next() — menyebabkan handler berikutnya tidak pernah dijalankan.
  2. Lupa memanggil c.Abort() setelah mengirim response error — request tetap lanjut ke handler meskipun sudah dianggap gagal di tengah jalan, berisiko menghasilkan dua response sekaligus.
  3. Menaruh logic berat di middleware global — misalnya query database besar yang dijalankan di setiap request tanpa terkecuali, padahal hanya dibutuhkan beberapa endpoint.
  4. Tidak konsisten memakai c.Set()/c.Get() — misalnya salah ketik key context di satu tempat, menyebabkan data tidak ditemukan di handler.
  5. Membuat middleware terlalu spesifik untuk satu endpoint — sebaiknya middleware tetap general-purpose dan dikombinasikan lewat grouping, bukan membuat satu middleware raksasa untuk satu use case saja.

Penutup

Middleware adalah salah satu fitur paling powerful di Gin untuk menjaga kode tetap DRY (Don’t Repeat Yourself) dan terorganisir. Dengan memahami cara kerja c.Next() dan c.Abort(), serta mempraktikkan pembuatan middleware custom seperti autentikasi, RBAC, rate limiting, hingga request ID, aplikasi Gin akan jauh lebih mudah dikembangkan secara modular — cukup “colok” middleware yang dibutuhkan di route atau group yang sesuai, tanpa perlu mengubah logic di dalam handler.

News