Pendahuluan

Go Fiber versi mayor terbaru telah semakin matang sebagai framework minimalis namun produktif untuk membangun REST API. Pada artikel ini kita membahas praktik terbaik agar API siap produksi: struktur response konsisten, payload validasi, pemisahan layer, dan penanganan error yang dapat diandalkan. Contoh fokus pada CRUD sederhana dengan pendekatan service layer untuk menjaga handler tetap tipis.

Prasyarat & Struktur Proyek

Mulai dari modul Go standar:

go mod init github.com/organisasi/api-fiber

Pasang dependensi kolektif:

go get github.com/gofiber/fiber/v3 github.com/gofiber/fiber/v3/middleware/logger github.com/gofiber/fiber/v3/middleware/recover github.com/google/uuid github.com/go-playground/validator/v10

Struktur direkomendasikan:

  • cmd/server/main.go untuk entry point dan konfigurasi Fiber
  • internal/handler untuk HTTP layer
  • internal/service untuk logika bisnis
  • internal/repository bila perlu akses data
  • pkg/response untuk pola respons seragam
  • internal/middleware untuk request ID, logging, recovery

Pemisahan ini membantu menjaga handler tetap tipis dan fokus mengorkestrasi service layer.

Middleware Produksi: Logging, Recovery, dan Request ID

Gunakan middleware Fiber bawaan plus custom request ID. Request ID memungkinkan Anda melacak permintaan di log dan sistem observability.

app.Use(logger.New(logger.Config{Format: "${time} | ${locals:requestid} | ${method} ${path} | ${status}", TimeFormat: "02/01/2006 15:04:05"}))
app.Use(recover.New())
app.Use(func(c *fiber.Ctx) error {
    requestID := c.Get(fiber.HeaderXRequestID)
    if requestID == "" {
        requestID = uuid.NewString()
    }
    c.Locals("requestid", requestID)
    c.Set(fiber.HeaderXRequestID, requestID)
    return c.Next()
})

Pastikan logger membaca locals:requestid agar setiap log mencantumkan ID yang sama. Recovery middleware Fiber menangani panic dan mengeluarkan HTTP 500, namun custom handler harus menyaring panic domain vs technical agar response tetap terstruktur.

Response API yang Konsisten

Struktur response yang konsisten memudahkan frontend atau klien memahami status. Gunakan pola umum:

type APIResponse struct {
    Status  string      `json:"status"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
    Error   interface{} `json:"error,omitempty"`
}

func JSONSuccess(c *fiber.Ctx, data interface{}, message string) error {
    return c.Status(fiber.StatusOK).JSON(APIResponse{Status: "success", Message: message, Data: data})
}

func JSONError(c *fiber.Ctx, status int, message string, err interface{}) error {
    return c.Status(status).JSON(APIResponse{Status: "error", Message: message, Error: err})
}

Dengan pola ini, client tidak perlu menebak letak data atau error di payload.

Validasi Payload dengan go-playground/validator

Gunakan validator untuk memaksa payload sesuai aturan bisnis. Selalu lakukan parsing JSON sebelum memanggil service layer.

type CreateBookRequest struct {
    Title  string `json:"title" validate:"required,min=3,max=120"`
    Author string `json:"author" validate:"required"
    Pages  int    `json:"pages" validate:"required,gt=0"
}

func validateStruct(req interface{}) error {
    v := validator.New()
    return v.Struct(req)
}

Handler bertanggung jawab parsing JSON, validasi, lalu memanggil service.

func (h *bookHandler) Create(c *fiber.Ctx) error {
    var req CreateBookRequest
    if err := c.BodyParser(&req); err != nil {
        return JSONError(c, fiber.StatusBadRequest, "payload tidak valid", map[string]string{"body": err.Error()})
    }
    if err := validateStruct(&req); err != nil {
        return JSONError(c, fiber.StatusBadRequest, "validasi gagal", err.Error())
    }
    book, err := h.service.CreateBook(c.Context(), req)
    if err != nil {
        return handleServiceError(c, err)
    }
    return JSONSuccess(c, book, "buku berhasil dibuat")
}

Pois: Pastikan BodyParser hanya dipanggil sekali per permintaan agar tidak terjadi error decode berlapis.

Memisahkan Handler dan Service Layer

Handler harus tipis; fokus pada HTTP-specific concerns seperti parsing dan response. Semua logika bisnis masuk service. Service layer juga mengembalikan error berjenis domain (misalnya ErrBookNotFound) vs technical (misalnya perintah database gagal). Contoh:

var ErrBookNotFound = errors.New("book not found")

func (s *bookService) GetBook(ctx context.Context, id string) (*Book, error) {
    book, err := s.repo.FindByID(ctx, id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, ErrBookNotFound
    }
    if err != nil {
        return nil, fmt.Errorf("repository: %w", err)
    }
    return book, nil
}

Service tidak berurusan langsung dengan HTTP. Handler menerjemahkan error domain menjadi response yang sesuai.

func handleServiceError(c *fiber.Ctx, err error) error {
    if errors.Is(err, ErrBookNotFound) {
        return JSONError(c, fiber.StatusNotFound, "buku tidak ditemukan", nil)
    }
    requestID := c.Locals("requestid")
    log.Printf("[%s] unexpected error: %v", requestID, err)
    return JSONError(c, fiber.StatusInternalServerError, "terjadi kesalahan server", map[string]string{"request_id": requestID.(string)})
}

Pemisahan ini membuat perilaku error lebih dapat diprediksi; error domain diterjemahkan ke status code yang relevan, sementara error teknis tetap dicatat dengan request ID untuk debugging.

Endpoint CRUD dengan Fiber

Gunakan route grouping agar lebih rapi:

api := app.Group("/api")
books := api.Group("/books")
books.Post("/", bookHandler.Create)
books.Get("/:id", bookHandler.Get)
books.Put("/:id", bookHandler.Update)
books.Delete("/:id", bookHandler.Delete)

Pastikan setiap handler mengandalkan service minimal bertanggung jawab hanya terhadap satu hal. Misalnya update:

func (h *bookHandler) Update(c *fiber.Ctx) error {
    vars := c.Params("id")
    var req UpdateBookRequest
    if err := c.BodyParser(&req); err != nil {
        return JSONError(c, fiber.StatusBadRequest, "payload tidak valid", nil)
    }
    if err := validate.Struct(&req); err != nil {
        return JSONError(c, fiber.StatusBadRequest, "validasi gagal", err.Error())
    }
    book, err := h.service.UpdateBook(c.Context(), vars, req)
    if err != nil {
        return handleServiceError(c, err)
    }
    return JSONSuccess(c, book, "buku berhasil diupdate")
}

Objek request update biasanya memuat pointer atau omitempty supaya service bisa membedakan field mana yang berubah.

Tips Debugging & Produksi

  • Gunakan log request ID untuk melacak error. Pastikan setiap respon error juga menyertakan request ID agar klien bisa melaporkan.
  • Jangan lupa middleware recover. Meski Fiber menangkap panic, Anda perlu tahu sebabnya. Gunakan logger yang menyimpan stack trace atau kirim ke Sentry.
  • Handle error domain vs teknis. Selalu wrap error teknis dengan konteks agar handler bisa membedakan dan menuliskan log terstruktur.
  • Pastikan validasi lengkap. Validasi tidak hanya input berbasis JSON, juga cek header, path params, serta authorization (jika ada).
  • Testing layer service dengan mock repository. Ini menjaga handler tetap tipis dan memastikan logika bisnis tidak tergantung HTTP.

Penutup

Membangun REST API production-ready dengan Go Fiber v3 berarti memperhatikan detail seperti pola response, validasi, middleware logging, error handling, dan pemisahan concern. Dengan handler tipis, service layer yang jelas, dan middleware request ID plus recovery, Anda memperoleh API yang mudah dipelihara sekaligus siap observability. Selalu evaluasi trade-off—misalnya menambahkan middleware tracing jika perlu, atau memperluas validasi untuk bisnis spesifik.