Menangani Response API yang Konsisten (Standard Response Format) di Golang Gin

Pendahuluan

Salah satu hal yang sering diabaikan saat membangun REST API dengan Gin adalah konsistensi format response. Di awal project, mungkin terasa tidak masalah kalau satu endpoint mengembalikan {"data": ...}, endpoint lain {"result": ...}, dan endpoint lain lagi langsung mengembalikan array tanpa wrapper sama sekali. Namun semakin project membesar dan semakin banyak tim frontend/mobile yang mengonsumsi API tersebut, ketidakkonsistenan ini akan menimbulkan banyak masalah: kode parsing di sisi client jadi tidak seragam, penanganan error jadi rumit, dan dokumentasi API jadi membingungkan.

Artikel ini membahas cara membangun standard response format di Gin, sehingga setiap endpoint — baik yang sukses maupun yang gagal — selalu mengembalikan struktur response yang sama.

Kenapa Response Format Perlu Distandarkan?

  1. Kemudahan bagi konsumer API — frontend/mobile developer cukup menulis satu fungsi parsing untuk semua endpoint.
  2. Penanganan error yang seragam — client bisa mendeteksi error dengan pola yang sama, tanpa perlu cek struktur berbeda-beda tiap endpoint.
  3. Dokumentasi lebih mudah — di Swagger/OpenAPI, cukup definisikan satu schema response yang dipakai berulang.
  4. Debugging lebih cepat — tim support/QA bisa langsung tahu di mana letak pesan error atau kode status tanpa harus membaca kode sumber tiap handler.

Merancang Struktur Response

Struktur response yang umum dipakai biasanya memiliki elemen berikut:

  • success — boolean penanda apakah request berhasil.
  • message — pesan yang bisa ditampilkan ke user atau untuk keperluan debugging.
  • data — isi data sebenarnya (bisa objek, array, atau null).
  • error — detail error, biasanya hanya diisi saat success: false.
  • meta — informasi tambahan seperti pagination (opsional).

Contoh response sukses:

json

{
  "success": true,
  "message": "User berhasil diambil",
  "data": {
    "id": 1,
    "name": "Budi Santoso",
    "email": "budi@example.com"
  }
}

Contoh response gagal:

json

{
  "success": false,
  "message": "Validasi gagal",
  "error": {
    "code": "VALIDATION_ERROR",
    "details": [
      { "field": "email", "message": "Format email tidak valid" }
    ]
  }
}

Contoh response dengan pagination:

json

{
  "success": true,
  "message": "Daftar produk berhasil diambil",
  "data": [
    { "id": 1, "name": "Produk A" },
    { "id": 2, "name": "Produk B" }
  ],
  "meta": {
    "page": 1,
    "limit": 10,
    "total_data": 42,
    "total_page": 5
  }
}

Implementasi di Golang Gin

1. Membuat Struct Response

Buat package khusus, misalnya internal/shared/response/response.go:

go

package response

type Meta struct {
    Page      int   `json:"page,omitempty"`
    Limit     int   `json:"limit,omitempty"`
    TotalData int64 `json:"total_data,omitempty"`
    TotalPage int   `json:"total_page,omitempty"`
}

type ErrorDetail struct {
    Field   string `json:"field,omitempty"`
    Message string `json:"message"`
}

type ErrorResponse struct {
    Code    string        `json:"code"`
    Details []ErrorDetail `json:"details,omitempty"`
}

type Response struct {
    Success bool           `json:"success"`
    Message string         `json:"message"`
    Data    interface{}    `json:"data,omitempty"`
    Error   *ErrorResponse `json:"error,omitempty"`
    Meta    *Meta          `json:"meta,omitempty"`
}

2. Membuat Helper Function

Agar tidak perlu menulis struct berulang-ulang di setiap handler, buat fungsi helper:

go

package response

import "github.com/gin-gonic/gin"

func Success(c *gin.Context, statusCode int, message string, data interface{}) {
    c.JSON(statusCode, Response{
        Success: true,
        Message: message,
        Data:    data,
    })
}

func SuccessWithMeta(c *gin.Context, statusCode int, message string, data interface{}, meta *Meta) {
    c.JSON(statusCode, Response{
        Success: true,
        Message: message,
        Data:    data,
        Meta:    meta,
    })
}

func Error(c *gin.Context, statusCode int, message string, code string, details []ErrorDetail) {
    c.JSON(statusCode, Response{
        Success: false,
        Message: message,
        Error: &ErrorResponse{
            Code:    code,
            Details: details,
        },
    })
}

3. Menggunakan Helper di Handler

go

func (h *UserHandler) GetUser(c *gin.Context) {
    id := c.Param("id")

    user, err := h.usecase.GetByID(c.Request.Context(), id)
    if err != nil {
        response.Error(c, http.StatusNotFound, "User tidak ditemukan", "USER_NOT_FOUND", nil)
        return
    }

    response.Success(c, http.StatusOK, "User berhasil diambil", user)
}

func (h *UserHandler) CreateUser(c *gin.Context) {
    var req dto.CreateUserRequest

    if err := c.ShouldBindJSON(&req); err != nil {
        response.Error(c, http.StatusBadRequest, "Validasi gagal", "VALIDATION_ERROR", parseValidationError(err))
        return
    }

    user, err := h.usecase.Create(c.Request.Context(), req)
    if err != nil {
        response.Error(c, http.StatusInternalServerError, "Gagal membuat user", "INTERNAL_ERROR", nil)
        return
    }

    response.Success(c, http.StatusCreated, "User berhasil dibuat", user)
}

Dengan pendekatan ini, setiap handler cukup memanggil response.Success atau response.Error tanpa perlu membangun struct gin.H{...} secara manual berulang kali, sehingga format response terjamin konsisten di seluruh aplikasi.

4. Menyeragamkan Error dari Panic/Recovery

Selain error yang ditangani manual di handler, error tak terduga (panic) juga sebaiknya melewati format yang sama. Ini bisa dilakukan lewat middleware recovery custom:

go

func RecoveryMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                response.Error(c, http.StatusInternalServerError, "Terjadi kesalahan pada server", "INTERNAL_ERROR", nil)
                c.Abort()
            }
        }()
        c.Next()
    }
}

Middleware ini didaftarkan menggantikan gin.Recovery() bawaan, sehingga meskipun terjadi panic di tengah proses, response yang dikembalikan ke client tetap mengikuti format standar, bukan pesan error mentah dari Go.

5. Menangani Error Validasi dengan Lebih Rapi

Saat menggunakan ShouldBindJSON, error validasi dari package validator biasanya berupa pesan teknis yang kurang ramah dibaca. Sebaiknya dibuat fungsi konversi agar pesan error lebih jelas per field:

go

func parseValidationError(err error) []response.ErrorDetail {
    var details []response.ErrorDetail

    if verrs, ok := err.(validator.ValidationErrors); ok {
        for _, fe := range verrs {
            details = append(details, response.ErrorDetail{
                Field:   fe.Field(),
                Message: fmt.Sprintf("%s tidak valid pada rule '%s'", fe.Field(), fe.Tag()),
            })
        }
    }

    return details
}

Menentukan Kode Error yang Konsisten

Selain HTTP status code, penggunaan code internal (seperti USER_NOT_FOUNDVALIDATION_ERRORUNAUTHORIZED) sangat membantu, terutama untuk:

  • Membedakan jenis error meskipun HTTP status code-nya sama (misalnya beberapa error berbeda sama-sama menghasilkan status 400).
  • Memudahkan frontend melakukan penanganan khusus berdasarkan kode, bukan mem-parsing pesan teks yang bisa berubah-ubah.
  • Memudahkan tim untuk membuat katalog error terpusat yang bisa didokumentasikan di Swagger.

Sebaiknya kode-kode ini didefinisikan sebagai konstanta di satu tempat, misalnya internal/shared/errors/codes.go, agar tidak ada penulisan string error yang berbeda-beda antar modul.

Praktik Tambahan

  • Jangan tampilkan detail error internal ke client di environment production (misalnya stack trace atau pesan error dari database), cukup log di server dan kirim pesan umum ke client.
  • Gunakan HTTP status code yang tepat — jangan selalu mengembalikan 200 OK meskipun success: false, tetap gunakan status code yang sesuai (400, 404, 401, 500, dst) agar sejalan dengan konvensi REST.
  • Pertimbangkan versi API (/api/v1/...) agar perubahan format response di masa depan tidak langsung memutus kompatibilitas client lama.
  • Sertakan format response ini dalam dokumentasi Swagger sebagai schema yang dipakai berulang (components/schemas di OpenAPI) supaya seluruh endpoint terdokumentasi secara seragam.

Penutup

Menstandarkan format response API bukan sekadar soal kerapian kode, tapi berdampak langsung pada pengalaman developer yang mengonsumsi API tersebut, baik itu tim frontend, mobile, maupun pihak ketiga. Dengan membuat struct Response terpusat dan helper function seperti response.Success dan response.Error, setiap handler di aplikasi Gin dapat mengembalikan format yang seragam tanpa duplikasi kode, sekaligus mempermudah maintenance dan dokumentasi API dalam jangka panjang.

News