30 Input Validation in graphql-go Mutations
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:
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:
| No | Validation Type | Example Rule |
|---|---|---|
| 1 | Required / Not Empty | name, email must be provided |
| 2 | Minimum Length | Password min. 8 characters |
| 3 | Maximum Length | Username max. 30 characters |
| 4 | Email Format | RFC5322 email validation |
| 5 | URL Format | Website URL validation |
| 6 | Custom Regex | Phone number matching a regex |
| 7 | Unique Value | Email must not be duplicated |
| 8 | Numeric Only | Digits only (postal code, etc.) |
| 9 | Number Range | Age: between 18 and 75 |
| 10 | Enum Choice Value | Status: ‘active’, ‘inactive’ |
| 11 | Valid Date | Format ‘YYYY-MM-DD’ |
| 12 | Must Not Be Negative | Amount, quantity |
| 13 | Default Value | When a field is not sent |
| 14 | Required Nested Field | data.profile.address required |
| 15 | Nested Array Validate | At least 1 item, etc. |
| 16 | UUID Format | id: valid uuid4 |
| 17 | No Special Character | Username validation |
| 18 | Conditional | If field A exists, field B is required |
| 19 | Array Length Min/Max | Minimum/maximum array items |
| 20 | CAPTCHA/Code Valid | External code validation |
| 21 | Required With | Field A required if field B exists |
| 22 | JSON Structure Format | Nested JSON matching the schema |
| 23 | Nested Object Validate | Validate every object in an array |
| 24 | Existence in DB | user id must exist in the DB |
| 25 | File Upload Type | Only .jpg, .png |
| 26 | File Upload Size | Maximum 2MB |
| 27 | RELATION Constraint | Foreign key exists |
| 28 | Unique Combination | username+email unique combination |
| 29 | List Contains Value | Must contain item X |
| 30 | Field Must Not Be Default Value | Must 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.
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:
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:
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 50email: Required, email format, unique in DBage: Required, range 18-65website: Optional, if present must be a valid URLroles: Array min 1, must be a valid enum (‘admin’, ‘user’, ’editor’)profile.bio: Max length 160profile.address: Required
Validation Flow Diagram (Mermaid Code)
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.
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.
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! 🚀