tutorial

113 Custom Validation di Resolver gqlgen

113 Custom Validation di Resolver gqlgen

Saat membangun API dengan GraphQL di Golang, salah satu package andalan adalah gqlgen. Framework ini punya kemampuan powerful dalam menyusun schema, membangun resolver, hingga auto-generation kode berdasarkan schema yang kita desain. Tapi ada satu masalah klasik: validasi data.

Sebagai engineer yang sudah beberapa kali memakai gqlgen, saya sadar validasi data—terutama custom validation—sering jadi area “abu-abu”. Tidak sedikit orang yang hanya mengandalkan validasi di layer atas (misal saat menerima request), atau bahkan baru memvalidasi sebelum operasi ke database. Padahal, letak validasi di resolver sendiri bisa jadi solusi performa dan arsitektur yang lebih solid.

Di artikel ini, saya akan breakdown 113 custom validation di Resolver gqlgen: apa, bagaimana, dan best practice-nya. Saya akan lengkapi dengan contoh kode, simulasi, tabel, serta diagram supaya kamu benar-benar paham dan bisa langsung aplikasikan di project-mu.


Mengapa Perlu Custom Validation di Resolver?

Sebelum masuk ke “bagaimana”, mari cari tahu “kenapa” dulu. Biasanya pertanyaan saya saat mendesain API adalah:

  • Apakah validasi perlu di schema (menggunakan directive)?
  • Apakah validasi better di model/database level saja?
  • Bagaimana dengan validasi yang sifatnya kompleks (cross-field, multi-model)?

Jika jawabannya pada salah satu kondisi di bawah, custom validation di resolver adalah jawabannya:

No.Kondisi Kebutuhan ValidasiSolusi Ideal
1Validasi spesifik yang tidak bisa diakomodasi directive/schema sajaCustom code di resolver
2Validasi yang melibatkan query ke data lain (cross-table/model validation)Resolver + repo/service
3Validasi yang berhubungan antara banyak field pada satu objekResolver (logic cross-field)
4Validasi yang output-nya error khusus (custom error message, status code)Resolver dengan error handling
5Validasi on-demand, hanya di certain mutation/queyLokal di resolver terkait

Jadi, validasi di resolver = fleksibel + powerful + bisa custom error handling.


Fondasi: Resolver di gqlgen

Sebelum bicara custom validation, sedikit refresh soal resolver di gqlgen.

# schema.graphqls
type Mutation {
  registerUser(input: RegisterUserInput!): UserResponse!
}

input RegisterUserInput {
  username: String!
  email: String!
  password: String!
}

Lalu di Go:

// resolver.go
func (r *mutationResolver) RegisterUser(ctx context.Context, input model.RegisterUserInput) (*model.UserResponse, error) {
  // Implementasi di sini, termasuk custom validation
}

Pola Basic: Custom Validation di Resolver

Pattern custom validation di resolver cukup sederhana:

  1. Ambil input dari argumen fungsi resolver
  2. Jalankan semua rules validasi (bisa dengan helper, struct tag, kondisi manual)
  3. Return error jika tidak valid

Contoh:

func (r *mutationResolver) RegisterUser(ctx context.Context, input model.RegisterUserInput) (*model.UserResponse, error) {
  if len(input.Username) < 5 {
    return nil, errors.New("username minimal 5 karakter")
  }
  if !strings.Contains(input.Email, "@") {
    return nil, errors.New("format email tidak valid")
  }
  if len(input.Password) < 8 {
    return nil, errors.New("password harus minimal 8 karakter")
  }
  
  // Validasi lainnya...

  // Proses user registration dan return UserResponse
}

Simulasi 113 Custom Validation

Saya akan simulasikan 10 kategori validasi (dengan total 113 rules) yang sering ditemui:

No.KategoriContoh RuleDeskripsi Singkat
1LengthMin/max karakterUsername min 5, password min 8
2Email formatValid email regexEmail harus valid
3Uniqueness DBEmail/username sudah adaQuery DB sebelum insert
4Strong password checkKombinasi angka, huruf besar/kecilPassword kuat
5Cross-field ruleKonfirmasi password matchpassword == confirmPassword
6Date/time validationValid tanggal lahirBukan future date
7Enum validationGender = [“male”, “female”, “other”]Validasi pilihan
8Nested object validationValidasi field dalam object complexAlamat lengkap
9Custom business rulesMinimal umur, status user aktifUmur > 17
10Custom error typeKode error, field error mappingPesan error field spesifik

Simulasi implementasi untuk 3 rules: format, unik di DB, konfirmasi password

func (r *mutationResolver) RegisterUser(ctx context.Context, input model.RegisterUserInput) (*model.UserResponse, error) {
  // 1. Email valid
  if !validateEmail(input.Email) {
    return nil, fmt.Errorf("Email '%s' tidak valid", input.Email)
  }

  // 2. Unik di DB
  exists, _ := r.UserRepo.IsEmailExist(ctx, input.Email)
  if exists {
    return nil, errors.New("Email sudah digunakan")
  }

  // 3. Konfirmasi password
  if input.Password != input.ConfirmPassword {
    return nil, errors.New("Konfirmasi password tidak sesuai")
  }

  // ... lanjutkan logic normal
}

func validateEmail(email string) bool {
    re := regexp.MustCompile(`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`)
    return re.MatchString(email)
}

Best Practice: Validasi Lebih Modular

Kebanyakan engineer akan kewalahan kalau ada lebih dari 5 validasi per endpoint. Supaya tetap clean, gunakan pattern modular validation.

Misal: Buat package validator/ dengan helper.

package validator

import (
    "regexp"
    "errors"
)

func ValidateRegisterUserInput(input *model.RegisterUserInput) error {
    if len(input.Username) < 5 {
      return errors.New("username min 5 karakter")
    }
    if !validateEmail(input.Email) {
      return errors.New("email format tidak valid")
    }
    // ... dan seterusnya
    return nil
}

Di resolver:

if err := validator.ValidateRegisterUserInput(&input); err != nil {
  return nil, err
}

Mapping Error ke Field (User Experience)

Error di GraphQL secara default akan dilemparkan jadi satu array. Namun, UX akan jauh lebih baik jika field error dikembalikan per field.

Schema:

type UserResponse {
  success: Boolean!
  user: User
  errors: [FieldError!]
}

type FieldError {
  field: String!
  message: String!
}

Resolver:

func (r *mutationResolver) RegisterUser(ctx context.Context, input model.RegisterUserInput) (*model.UserResponse, error) {
  var errors []*model.FieldError

  if len(input.Username) < 5 {
    errors = append(errors, &model.FieldError{Field: "username", Message: "username min 5 karakter"})
  }
  if !validateEmail(input.Email) {
    errors = append(errors, &model.FieldError{Field: "email", Message: "format email tidak valid"})
  }

  if len(errors) > 0 {
    return &model.UserResponse{Success: false, Errors: errors}, nil
  }

  // lanjutkan proses...
}

Diagram Alur Validasi di Resolver (Mermaid)

Kadang gambar lebih mudah membekas. Berikut flow custom validation di resolver gqlgen.

flowchart TD
    A[Receive GraphQL Mutation] --> B[Parse Input]
    B --> C[Custom Validation di Resolver]
    C -->|Valid| D[Proses Data (DB, Service, dsb)]
    C -->|Invalid| E[Return Error/FieldError]
    D --> F[Return Response (User data, Success)]
    E --> F

Kesimpulan

Custom validation di resolver gqlgen sebetulnya bukan sekadar fitur, melainkan salah satu kunci menjaga data integrity di aplikasi kita. Dengan 113 rules (atau bahkan lebih), kamu bisa atur step-by-step logic validasi dari yang paling sederhana hingga kompleks—termasuk pengembalian error yang ramah UX.

Saran saya:

  • Jangan overload resolver-mu; gunakan modular helper.
  • Gunakan mapping field error agar consumers mudah paham gagal di mana.
  • Validasi di resolver adalah mid-layer yang melindungi data — jangan cuma berharap pada validasi schema atau DB constraint.

Selamat mencoba, semoga API gqlgen-mu makin kokoh dan maintainable!

comments powered by Disqus