Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
30 Jul 2025 · 7 min read ·Article 30 / 125
Go

30 Input Validation in graphql-go Mutations

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

30 Input Validation in graphql-go Mutations

In today’s fast-paced and highly connected world of modern application development, GraphQL has become a top choice for building APIs thanks to its flexibility and efficiency. However, developers often focus too heavily on schema and resolver design, and tend to overlook another crucial aspect: input validation, especially in mutation operations.

This article will discuss how to add and manage 30 input validations on mutations using the graphql-go library in the Go programming language. We will also cover code examples, simulations, a validation table, and an architectural blueprint for organizing validation in a clean way.


Why Do We Need Validation at the Mutation Layer?

Input validation is the process of ensuring that the data received by an API conforms to the expected rules, business logic, and data types. In the context of a GraphQL mutation, input validation acts as the gateway that keeps corrupt data from penetrating deeper into the database, while preserving the integrity of the system.

Without proper validation, problems such as data inconsistency, runtime errors, and even security vulnerabilities frequently arise.


A Quick Look at graphql-go

graphql-go is one of the popular libraries in Go for building a GraphQL server. With it, developers define schemas and resolvers, but validation remains entirely our own responsibility.

As an illustration, here is a simple mutation definition in graphql-go:

go
 1var createUserMutation = &graphql.Field{
 2	Type: userType,
 3	Args: graphql.FieldConfigArgument{
 4		"name":    &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
 5		"email":   &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
 6		"age":     &graphql.ArgumentConfig{Type: graphql.Int},
 7		"website": &graphql.ArgumentConfig{Type: graphql.String},
 8	},
 9	Resolve: createUserResolver,
10}

However, input validation is not performed automatically by this library—everything still happens inside the resolver.


30 Types of Input Validation for Mutations

Below is a list of 30 common and important input validations that are frequently applied to GraphQL mutations:

NoValidation TypeExample Rule
1Required / Not Emptyname, email must be provided
2Minimum LengthPassword min. 8 characters
3Maximum LengthUsername max. 30 characters
4Email FormatRFC5322 email validation
5URL FormatWebsite URL validation
6Custom RegexPhone number matching a regex
7Unique ValueEmail must not be duplicated
8Numeric OnlyDigits only (postal code, etc.)
9Number RangeAge: between 18 and 75
10Enum Choice ValueStatus: ‘active’, ‘inactive’
11Valid DateFormat ‘YYYY-MM-DD’
12Must Not Be NegativeAmount, quantity
13Default ValueWhen a field is not sent
14Required Nested Fielddata.profile.address required
15Nested Array ValidateAt least 1 item, etc.
16UUID Formatid: valid uuid4
17No Special CharacterUsername validation
18ConditionalIf field A exists, field B is required
19Array Length Min/MaxMinimum/maximum array items
20CAPTCHA/Code ValidExternal code validation
21Required WithField A required if field B exists
22JSON Structure FormatNested JSON matching the schema
23Nested Object ValidateValidate every object in an array
24Existence in DBuser id must exist in the DB
25File Upload TypeOnly .jpg, .png
26File Upload SizeMaximum 2MB
27RELATION ConstraintForeign key exists
28Unique Combinationusername+email unique combination
29List Contains ValueMust contain item X
30Field Must Not Be Default ValueMust not be ’test’, ‘dummy’

Managing Validation: Clean and Maintainable

So that the 30 rules above don’t turn the resolver into a pile of if-else statements and nested code, the clean architecture pattern is essential.

I usually create a dedicated package, for example validation, and turn each validation into a reusable function.

go
 1package validation
 2
 3import (
 4	"errors"
 5	"regexp"
 6)
 7
 8func RequiredString(s, field string) error {
 9	if s == "" {
10		return errors.New(field + " is required")
11	}
12	return nil
13}
14
15func ValidateEmail(email string) error {
16	regex := `^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`
17	if !regexp.MustCompile(regex).MatchString(email) {
18		return errors.New("Invalid email")
19	}
20	return nil
21}

Then, in the resolver, the validations are composed:

go
 1func createUserResolver(p graphql.ResolveParams) (interface{}, error) {
 2	name, _ := p.Args["name"].(string)
 3	email, _ := p.Args["email"].(string)
 4	age, _ := p.Args["age"].(int)
 5	website, _ := p.Args["website"].(string)
 6
 7	// 1. Required
 8	if err := validation.RequiredString(name, "name"); err != nil {
 9		return nil, err
10	}
11	// 2. Email format
12	if err := validation.ValidateEmail(email); err != nil {
13		return nil, err
14	}
15	// 3. Age range
16	if age < 18 || age > 75 {
17		return nil, errors.New("Age must be between 18 and 75 years")
18	}
19	// 4. Website format
20	if website != "" {
21		if err := validation.ValidateURL(website); err != nil {
22			return nil, err
23		}
24	}
25	// ... and so on (up to 30 validations)
26	return User{...}, nil
27}

Simulation: One Payload, Many Validations

In a large application, a single mutation can involve several levels of validation. The payload simulation below demonstrates validation across multiple levels.

Example Payload:

json
 1{
 2  "name": "John Doe",
 3  "email": "john_doe@email.com",
 4  "age": 17,
 5  "website": "not-a-url",
 6  "roles": ["admin", "super_user"],
 7  "profile": {
 8    "bio": "",
 9    "address": "Jl. Sudirman"
10  }
11}

Validation Layers

  • name: Required, max length 50
  • email: Required, email format, unique in DB
  • age: Required, range 18-65
  • website: Optional, if present must be a valid URL
  • roles: Array min 1, must be a valid enum (‘admin’, ‘user’, ’editor’)
  • profile.bio: Max length 160
  • profile.address: Required

Validation Flow Diagram (Mermaid Code)

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: Validation Aggregator

To collect every error, the error aggregator pattern is extremely helpful. We can store all validation error messages and, at the end, only return them if the error list is not empty.

go
 1var validationErrs []string
 2
 3if err := validation.RequiredString(name, "name"); err != nil {
 4	validationErrs = append(validationErrs, err.Error())
 5}
 6// ... all validations
 7
 8if len(validationErrs) > 0 {
 9	return nil, errors.New(strings.Join(validationErrs, "; "))
10}

Testing: Simulate the Request

To make sure the validation works, write tests using table-driven tests for each rule.

go
 1func TestValidateCreateUserInput(t *testing.T) {
 2	tests := []struct{
 3		payload  map[string]interface{}
 4		wantErr  bool
 5	}{
 6		{
 7			payload: map[string]interface{}{"name": "", "email": "salah@", "age": 10},
 8			wantErr: true,
 9		},
10		{
11			payload: map[string]interface{}{"name": "Budi", "email": "budi@mail.com", "age": 20},
12			wantErr: false,
13		},
14		// and so on, following the 30-validation checklist
15	}
16}

Conclusion

Embedding 30 mutation input validations in graphql-go—and scaling validation across dozens of fields—demands architectural discipline around clean design as well as creativity in composing validation functions. Validation isn’t just strict on the data type side; it also covers custom rules, conditional logic, and DB relations.

With an approach built on reusable validation functions, an error aggregator, and user-friendly errors, the security and reliability of your API will improve dramatically.

Closing thoughts:
Don’t take mutation input validation lightly. Investing time upfront will save you from a long list of bugs, attacks, and even downtime down the road.


References


I hope this article helps you, and—as always—keep your codebase robust and clean! 🚀

Related Articles

💬 Comments