tutorial

30 Validasi Input di Mutation graphql-go

30 Validasi Input di Mutation graphql-go

Pada era perkembangan aplikasi modern yang serba cepat dan terhubung, GraphQL telah menjadi pilihan utama dalam membangun API karena fleksibilitas dan efisiensinya. Namun, sering kali developer terlalu fokus pada desain schema dan resolver, lalu agak lalai pada aspek penting lainnya: validasi input, terutama pada operasi mutation.

Artikel ini akan membahas bagaimana menambahkan dan mengelola 30 validasi input pada mutation menggunakan library graphql-go di bahasa pemrograman Go. Kita juga akan membahas contoh kode, simulasi, tabel validasi, dan blueprint arsitektur penyusunan validasi secara clean.


Mengapa Perlu Validasi di Layer Mutation?

Validasi input adalah proses memastikan data yang diterima oleh API telah sesuai dengan aturan, logika bisnis, maupun tipe data yang diharapkan. Dalam context mutation GraphQL, validasi input menjadi pintu gerbang agar data corrupt tidak menembus lebih dalam ke database, serta menjaga integrity sistem.

Tanpa validasi yang baik, masalah seperti data inconsistency, runtime error hingga security vulnerability seringkali muncul.


Sekilas Tentang graphql-go

graphql-go adalah salah satu library populer di Go untuk membangun GraphQL server. Di sini, developer mendefinisikan schema dan resolver, namun urusan validasi tetap menjadi tanggung jawab kita.

Sebagai ilustrasi, definisi mutation sederhana di graphql-go:

var createUserMutation = &graphql.Field{
	Type: userType,
	Args: graphql.FieldConfigArgument{
		"name":    &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
		"email":   &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
		"age":     &graphql.ArgumentConfig{Type: graphql.Int},
		"website": &graphql.ArgumentConfig{Type: graphql.String},
	},
	Resolve: createUserResolver,
}

Namun, validasi input tidak otomatis dilakukan library ini—semua tetap di-resolver.


30 Jenis Validasi Input untuk Mutation

Berikut adalah daftar 30 validasi input umum yang sering dan penting diterapkan pada mutation GraphQL:

NoJenis ValidasiContoh Aturan
1Required / Tidak Boleh Kosongname, email wajib diisi
2Panjang MinimumPassword min. 8 karakter
3Panjang MaksimumUsername max. 30 karakter
4Format EmailValidasi email RFC5322
5Format URLValidasi website URL
6Regex CustomNomor telepon sesuai regex
7Nilai UnikEmail tidak boleh duplikat
8Numeric OnlyHanya angka (kode pos, dsb)
9Range AngkaAge: antara 18 dan 75
10Nilai Pilihan EnumStatus: ‘active’, ‘inactive’
11Tanggal ValidFormat ‘YYYY-MM-DD’
12Tidak Boleh NegatifNominal, jumlah
13Nilai DefaultJika field tidak dikirim
14Field Nested Wajibdata.profile.address wajib
15Nested Array ValidateMinimal 1 item, dsb
16Format UUIDid: uuid4 valid
17No Special CharacterValidasi username
18KondisionalJika field A ada, field B wajib
19Array Length Min/MaxMinimal/maksimal item array
20CAPTCHA/Code ValidValidasi kode eksternal
21Required WithField A wajib kalau field B ada
22JSON Structure FormatNested JSON sesuai schema
23Nested Object ValidateValidasi setiap object dalam array
24Existence in DBid user harus exist di DB
25File Upload TypeHanya .jpg, .png
26File Upload SizeMaksimal 2MB
27RELATION ConstraintForeign key exist
28Unique Combinationusername+email kombinasi unik
29List Contains ValueHarus mengandung item X
30Field Tidak Boleh Nilai DefaultTidak boleh ’test’, ‘dummy’

Pengelolaan Validasi: Clean dan Maintainable

Agar validasi 30 aturan di atas tidak menyebabkan resolver penuh if-else dan nested code, pattern clean architecture sangat diperlukan.

Saya biasanya membuat satu package khusus, contoh: validation, lalu setiap validasi dijadikan fungsi reusable.

package validation

import (
	"errors"
	"regexp"
)

func RequiredString(s, field string) error {
	if s == "" {
		return errors.New(field + " wajib diisi")
	}
	return nil
}

func ValidateEmail(email string) error {
	regex := `^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`
	if !regexp.MustCompile(regex).MatchString(email) {
		return errors.New("Email tidak valid")
	}
	return nil
}

Kemudian di resolver, validasi di-compose:

func createUserResolver(p graphql.ResolveParams) (interface{}, error) {
	name, _ := p.Args["name"].(string)
	email, _ := p.Args["email"].(string)
	age, _ := p.Args["age"].(int)
	website, _ := p.Args["website"].(string)

	// 1. Required
	if err := validation.RequiredString(name, "name"); err != nil {
		return nil, err
	}
	// 2. Email format
	if err := validation.ValidateEmail(email); err != nil {
		return nil, err
	}
	// 3. Age range
	if age < 18 || age > 75 {
		return nil, errors.New("Umur harus 18-75 tahun")
	}
	// 4. Website format
	if website != "" {
		if err := validation.ValidateURL(website); err != nil {
			return nil, err
		}
	}
	// ... dan seterusnya (hingga 30 validasi)
	return User{...}, nil
}

Simulasi: Satu Payload, Banyak Validasi

Kalau aplikasi besar, satu mutation bisa mengandung beberapa level validasi. Simulasi payload di bawah ini akan menunjukkan validasi di berbagai level.

Contoh Payload:

{
  "name": "John Doe",
  "email": "john_doe@email.com",
  "age": 17,
  "website": "not-a-url",
  "roles": ["admin", "super_user"],
  "profile": {
    "bio": "",
    "address": "Jl. Sudirman"
  }
}

Layer Validasi

  • name: Required, length max 50
  • email: Required, email format, unique in DB
  • age: Required, range 18-65
  • website: Optional, jika ada harus URL yang valid
  • roles: Array min 1, harus enum valid (‘admin’, ‘user’, ’editor’)
  • profile.bio: Panjang max 160
  • profile.address: Required

Diagram Alur Validasi (Kode Mermaid)

flowchart TD
    Start --> CekName[Validasi name]
    CekName -- OK --> CekEmail[Validasi email]
    CekEmail -- OK --> CekAge[Validasi age]
    CekAge -- OK --> CekWebsite[Validasi website]
    CekWebsite -- OK --> CekRoles[Validasi roles]
    CekRoles -- OK --> CekProfileBio[Validasi profile.bio]
    CekProfileBio -- OK --> CekProfileAddress[Validasi profile.address]
    CekProfileAddress -- OK --> Sukses
    CekName -- ERROR --> Error["Return error"]
    CekEmail -- ERROR --> Error
    CekAge -- ERROR --> Error
    CekWebsite -- ERROR --> Error
    CekRoles -- ERROR --> Error
    CekProfileBio -- ERROR --> Error
    CekProfileAddress -- ERROR --> Error

Pattern: Validasi Aggregator

Agar setiap error tersebar, pattern error aggregator sangat membantu. Kita bisa menyimpan semua pesan error validasi, lalu di akhir hanya return jika error tidak kosong.

var validationErrs []string

if err := validation.RequiredString(name, "name"); err != nil {
	validationErrs = append(validationErrs, err.Error())
}
// ... semua validasi

if len(validationErrs) > 0 {
	return nil, errors.New(strings.Join(validationErrs, "; "))
}

Testing: Simulasikan Request

Untuk memastikan validasi bekerja, buatlah test dengan table driven test untuk setiap aturan.

func TestValidateCreateUserInput(t *testing.T) {
	tests := []struct{
		payload  map[string]interface{}
		wantErr  bool
	}{
		{
			payload: map[string]interface{}{"name": "", "email": "salah@", "age": 10},
			wantErr: true,
		},
		{
			payload: map[string]interface{}{"name": "Budi", "email": "budi@mail.com", "age": 20},
			wantErr: false,
		},
		// dst, sesuai checklist 30 validasi
	}
}

Kesimpulan

Menanamkan 30 validasi input mutation di graphql-go—dan scalable validasi untuk puluhan field—menuntut disiplin arsitektur clean serta kreatifitas dalam compose fungsi validasi. Validasi bukan hanya tegas pada sisi data type, namun juga pada custom rule, kondisional, maupun relasi DB.

Dengan pendekatan reusable validation function, error aggregator, dan error yang user-friendly, keamanan serta reliability API akan jauh meningkat.

Penutup:
Jangan anggap enteng validasi input mutation. Investasi waktu di awal akan menghindarkan deretan bug, serangan, hingga downtime di masa depan.


Referensi


Semoga artikel ini membantumu, dan—seperti biasa—jaga codebase-mu tetap robust dan clean! 🚀

comments powered by Disqus