CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-github-com--graphql-go--graphql

Complete GraphQL implementation in Go with schema definition, query execution, and validation.

Overview
Eval results
Files

real-world-scenarios.mddocs/examples/

Real-World Scenarios

Complete API with Mutations

package main

import (
    "context"
    "github.com/graphql-go/graphql"
)

// Data models
type User struct {
    ID    string
    Name  string
    Email string
}

type Post struct {
    ID       string
    Title    string
    AuthorID string
}

// In-memory storage
var users = map[string]*User{}
var posts = map[string]*Post{}

func main() {
    // Define User type
    userType := graphql.NewObject(graphql.ObjectConfig{
        Name: "User",
        Fields: graphql.Fields{
            "id":    &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
            "name":  &graphql.Field{Type: graphql.String},
            "email": &graphql.Field{Type: graphql.String},
            "posts": &graphql.Field{
                Type: graphql.NewList(postType), // Forward reference - use thunk in production
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    user := p.Source.(*User)
                    userPosts := []*Post{}
                    for _, post := range posts {
                        if post.AuthorID == user.ID {
                            userPosts = append(userPosts, post)
                        }
                    }
                    return userPosts, nil
                },
            },
        },
    })

    // Define Post type
    postType := graphql.NewObject(graphql.ObjectConfig{
        Name: "Post",
        Fields: graphql.Fields{
            "id":    &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
            "title": &graphql.Field{Type: graphql.String},
            "author": &graphql.Field{
                Type: userType,
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    post := p.Source.(*Post)
                    return users[post.AuthorID], nil
                },
            },
        },
    })

    // Query type
    queryType := graphql.NewObject(graphql.ObjectConfig{
        Name: "Query",
        Fields: graphql.Fields{
            "user": &graphql.Field{
                Type: userType,
                Args: graphql.FieldConfigArgument{
                    "id": &graphql.ArgumentConfig{
                        Type: graphql.NewNonNull(graphql.ID),
                    },
                },
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    id := p.Args["id"].(string)
                    user, exists := users[id]
                    if !exists {
                        return nil, fmt.Errorf("user not found: %s", id)
                    }
                    return user, nil
                },
            },
            "users": &graphql.Field{
                Type: graphql.NewList(userType),
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    userList := []*User{}
                    for _, user := range users {
                        userList = append(userList, user)
                    }
                    return userList, nil
                },
            },
        },
    })

    // Mutation type
    mutationType := graphql.NewObject(graphql.ObjectConfig{
        Name: "Mutation",
        Fields: graphql.Fields{
            "createUser": &graphql.Field{
                Type: userType,
                Args: graphql.FieldConfigArgument{
                    "name": &graphql.ArgumentConfig{
                        Type: graphql.NewNonNull(graphql.String),
                    },
                    "email": &graphql.ArgumentConfig{
                        Type: graphql.NewNonNull(graphql.String),
                    },
                },
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    name := p.Args["name"].(string)
                    email := p.Args["email"].(string)
                    
                    user := &User{
                        ID:    generateID(),
                        Name:  name,
                        Email: email,
                    }
                    users[user.ID] = user
                    return user, nil
                },
            },
        },
    })

    // Create schema
    schema, err := graphql.NewSchema(graphql.SchemaConfig{
        Query:    queryType,
        Mutation: mutationType,
    })
    if err != nil {
        panic(err)
    }

    // Execute query
    result := graphql.Do(graphql.Params{
        Schema:        schema,
        RequestString: `mutation { createUser(name: "Alice", email: "alice@example.com") { id name } }`,
    })
}

Handling Circular References with Thunks

var personType *graphql.Object
var companyType *graphql.Object

// Use thunks to defer field resolution
personType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Person",
    Fields: graphql.FieldsThunk(func() graphql.Fields {
        return graphql.Fields{
            "name":    &graphql.Field{Type: graphql.String},
            "company": &graphql.Field{Type: companyType}, // Can reference companyType now
        }
    }),
})

companyType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Company",
    Fields: graphql.FieldsThunk(func() graphql.Fields {
        return graphql.Fields{
            "name":      &graphql.Field{Type: graphql.String},
            "employees": &graphql.Field{Type: graphql.NewList(personType)}, // Can reference personType
        }
    }),
})

Interface Implementation

// Define interface
nodeInterface := graphql.NewInterface(graphql.InterfaceConfig{
    Name: "Node",
    Fields: graphql.Fields{
        "id": &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
    },
    ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object {
        // Determine concrete type based on value
        switch p.Value.(type) {
        case *User:
            return userType
        case *Post:
            return postType
        default:
            return nil
        }
    },
})

// Implement interface
userType := graphql.NewObject(graphql.ObjectConfig{
    Name: "User",
    Interfaces: []*graphql.Interface{nodeInterface},
    Fields: graphql.Fields{
        "id":   &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
        "name": &graphql.Field{Type: graphql.String},
    },
    IsTypeOf: func(p graphql.IsTypeOfParams) bool {
        _, ok := p.Value.(*User)
        return ok
    },
})

Union Types

// Define union
searchResultUnion := graphql.NewUnion(graphql.UnionConfig{
    Name:  "SearchResult",
    Types: []*graphql.Object{userType, postType, commentType},
    ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object {
        switch p.Value.(type) {
        case *User:
            return userType
        case *Post:
            return postType
        case *Comment:
            return commentType
        default:
            return nil
        }
    },
})

// Query returning union
fields := graphql.Fields{
    "search": &graphql.Field{
        Type: graphql.NewList(searchResultUnion),
        Args: graphql.FieldConfigArgument{
            "query": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
        },
        Resolve: func(p graphql.ResolveParams) (interface{}, error) {
            query := p.Args["query"].(string)
            return performSearch(query), nil // Returns []interface{} with mixed types
        },
    },
}

// Query with inline fragments:
// { search(query: "test") { ... on User { name } ... on Post { title } } }

Custom Scalars

import "time"

// Define custom Date scalar
dateScalar := graphql.NewScalar(graphql.ScalarConfig{
    Name: "Date",
    Description: "Date in YYYY-MM-DD format",
    Serialize: func(value interface{}) interface{} {
        switch v := value.(type) {
        case time.Time:
            return v.Format("2006-01-02")
        case string:
            return v
        default:
            return nil
        }
    },
    ParseValue: func(value interface{}) interface{} {
        switch v := value.(type) {
        case string:
            t, err := time.Parse("2006-01-02", v)
            if err != nil {
                return nil
            }
            return t
        default:
            return nil
        }
    },
    ParseLiteral: func(valueAST ast.Value) interface{} {
        if strValue, ok := valueAST.(*ast.StringValue); ok {
            t, err := time.Parse("2006-01-02", strValue.Value)
            if err != nil {
                return nil
            }
            return t
        }
        return nil
    },
})

// Use in schema
fields := graphql.Fields{
    "createdAt": &graphql.Field{Type: dateScalar},
}

Enum Types

episodeEnum := graphql.NewEnum(graphql.EnumConfig{
    Name: "Episode",
    Values: graphql.EnumValueConfigMap{
        "NEWHOPE": &graphql.EnumValueConfig{
            Value:       4,
            Description: "Released in 1977.",
        },
        "EMPIRE": &graphql.EnumValueConfig{
            Value:       5,
            Description: "Released in 1980.",
        },
        "JEDI": &graphql.EnumValueConfig{
            Value:       6,
            Description: "Released in 1983.",
        },
    },
})

// Use in field
fields := graphql.Fields{
    "hero": &graphql.Field{
        Type: characterType,
        Args: graphql.FieldConfigArgument{
            "episode": &graphql.ArgumentConfig{Type: episodeEnum},
        },
        Resolve: func(p graphql.ResolveParams) (interface{}, error) {
            episode := p.Args["episode"].(int) // Gets the Value (4, 5, or 6)
            return getHero(episode), nil
        },
    },
}

Input Objects

// Define input object for complex arguments
userInputType := graphql.NewInputObject(graphql.InputObjectConfig{
    Name: "UserInput",
    Fields: graphql.InputObjectConfigFieldMap{
        "name": &graphql.InputObjectFieldConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "email": &graphql.InputObjectFieldConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "age": &graphql.InputObjectFieldConfig{
            Type:         graphql.Int,
            DefaultValue: 0,
        },
    },
})

// Use in mutation
fields := graphql.Fields{
    "createUser": &graphql.Field{
        Type: userType,
        Args: graphql.FieldConfigArgument{
            "input": &graphql.ArgumentConfig{
                Type: graphql.NewNonNull(userInputType),
            },
        },
        Resolve: func(p graphql.ResolveParams) (interface{}, error) {
            input := p.Args["input"].(map[string]interface{})
            name := input["name"].(string)
            email := input["email"].(string)
            age := input["age"].(int)
            
            return createUser(name, email, age), nil
        },
    },
}

// Mutation: createUser(input: {name: "Alice", email: "alice@example.com", age: 30})

Error Handling Patterns

// Return error - field becomes null, error in Result.Errors
resolve: func(p graphql.ResolveParams) (interface{}, error) {
    user, err := fetchUser(p.Args["id"].(string))
    if err != nil {
        return nil, err // Field is null, error reported
    }
    return user, nil
}

// Partial results - return data AND error
resolve: func(p graphql.ResolveParams) (interface{}, error) {
    users, err := fetchUsers()
    if err != nil {
        return users, err // Returns partial data + error
    }
    return users, nil
}

// Custom error with extensions
type CustomError struct {
    msg  string
    code string
}

func (e *CustomError) Error() string { return e.msg }
func (e *CustomError) Extensions() map[string]interface{} {
    return map[string]interface{}{"code": e.code}
}

resolve: func(p graphql.ResolveParams) (interface{}, error) {
    return nil, &CustomError{msg: "not found", code: "NOT_FOUND"}
    // Result.Errors[0].Extensions = {"code": "NOT_FOUND"}
}

Subscriptions

import "time"

// Message channel (in real app, this would be a message broker)
var messageChan = make(chan *Message, 100)

// Define subscription type
subscriptionType := graphql.NewObject(graphql.ObjectConfig{
    Name: "Subscription",
    Fields: graphql.Fields{
        "messageAdded": &graphql.Field{
            Type: messageType,
            Subscribe: func(p graphql.ResolveParams) (interface{}, error) {
                // Return a channel that emits values
                ch := make(chan interface{})
                go func() {
                    defer close(ch)
                    for msg := range messageChan {
                        ch <- msg
                    }
                }()
                return ch, nil
            },
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                // Transform the channel value if needed
                return p.Source, nil
            },
        },
    },
})

// Execute subscription
resultChan := graphql.Subscribe(graphql.Params{
    Schema:        schema,
    RequestString: `subscription { messageAdded { id text timestamp } }`,
})

// Consume results
for result := range resultChan {
    if result.HasErrors() {
        log.Printf("errors: %v", result.Errors)
        continue
    }
    // Process result.Data
    json.NewEncoder(os.Stdout).Encode(result.Data)
}

Context for Authentication

type contextKey string

const userIDKey contextKey = "userID"

// Middleware adds userID to context
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := extractUserID(r) // From JWT, session, etc.
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Resolver uses context
resolve: func(p graphql.ResolveParams) (interface{}, error) {
    userID, ok := p.Context.Value(userIDKey).(string)
    if !ok || userID == "" {
        return nil, fmt.Errorf("unauthorized")
    }
    return fetchUserData(userID), nil
}

Pagination Pattern

// Connection type for pagination
connectionType := graphql.NewObject(graphql.ObjectConfig{
    Name: "UserConnection",
    Fields: graphql.Fields{
        "edges": &graphql.Field{
            Type: graphql.NewList(graphql.NewObject(graphql.ObjectConfig{
                Name: "UserEdge",
                Fields: graphql.Fields{
                    "node":   &graphql.Field{Type: userType},
                    "cursor": &graphql.Field{Type: graphql.String},
                },
            })),
        },
        "pageInfo": &graphql.Field{
            Type: graphql.NewObject(graphql.ObjectConfig{
                Name: "PageInfo",
                Fields: graphql.Fields{
                    "hasNextPage":     &graphql.Field{Type: graphql.Boolean},
                    "hasPreviousPage": &graphql.Field{Type: graphql.Boolean},
                    "startCursor":     &graphql.Field{Type: graphql.String},
                    "endCursor":       &graphql.Field{Type: graphql.String},
                },
            }),
        },
    },
})

// Query with pagination
fields := graphql.Fields{
    "users": &graphql.Field{
        Type: connectionType,
        Args: graphql.FieldConfigArgument{
            "first": &graphql.ArgumentConfig{Type: graphql.Int},
            "after": &graphql.ArgumentConfig{Type: graphql.String},
        },
        Resolve: func(p graphql.ResolveParams) (interface{}, error) {
            first := 10
            if val, ok := p.Args["first"]; ok {
                first = val.(int)
            }
            
            after := ""
            if val, ok := p.Args["after"]; ok {
                after = val.(string)
            }
            
            return fetchUsersPage(first, after), nil
        },
    },
}

DataLoader Pattern (N+1 Prevention)

// Batch loading function
type UserLoader struct {
    cache map[string]*User
    mu    sync.Mutex
}

func (l *UserLoader) Load(ids []string) ([]*User, error) {
    l.mu.Lock()
    defer l.mu.Unlock()
    
    // Check cache first
    uncached := []string{}
    for _, id := range ids {
        if _, exists := l.cache[id]; !exists {
            uncached = append(uncached, id)
        }
    }
    
    // Batch fetch uncached
    if len(uncached) > 0 {
        users, err := batchFetchUsers(uncached)
        if err != nil {
            return nil, err
        }
        for _, user := range users {
            l.cache[user.ID] = user
        }
    }
    
    // Return in requested order
    result := make([]*User, len(ids))
    for i, id := range ids {
        result[i] = l.cache[id]
    }
    return result, nil
}

// Add loader to context
ctx := context.WithValue(context.Background(), "userLoader", &UserLoader{cache: make(map[string]*User)})

// Use in resolver
resolve: func(p graphql.ResolveParams) (interface{}, error) {
    loader := p.Context.Value("userLoader").(*UserLoader)
    post := p.Source.(*Post)
    users, err := loader.Load([]string{post.AuthorID})
    if err != nil || len(users) == 0 {
        return nil, err
    }
    return users[0], nil
}

Validation Before Execution

import (
    "github.com/graphql-go/graphql"
    "github.com/graphql-go/graphql/language/parser"
)

// Parse query
doc, err := parser.Parse(parser.ParseParams{Source: queryString})
if err != nil {
    return fmt.Errorf("parse error: %w", err)
}

// Validate before executing
validationResult := graphql.ValidateDocument(&schema, doc, nil)
if !validationResult.IsValid {
    for _, err := range validationResult.Errors {
        log.Printf("validation error: %s", err.Message)
    }
    return fmt.Errorf("validation failed")
}

// Execute validated document
result := graphql.Execute(graphql.ExecuteParams{
    Schema: schema,
    AST:    doc,
})

Directives

// Define custom directive
authDirective := graphql.NewDirective(graphql.DirectiveConfig{
    Name: "auth",
    Description: "Requires authentication",
    Locations: []string{
        graphql.DirectiveLocationFieldDefinition,
    },
    Args: graphql.FieldConfigArgument{
        "requires": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
    },
})

// Add to schema
schema, _ := graphql.NewSchema(graphql.SchemaConfig{
    Query:      queryType,
    Directives: append(graphql.SpecifiedDirectives, authDirective),
})

// Note: Directive execution requires custom middleware/extensions

Type Coercion and Validation

// Always validate and coerce argument types
resolve: func(p graphql.ResolveParams) (interface{}, error) {
    // String argument
    id, ok := p.Args["id"].(string)
    if !ok {
        return nil, fmt.Errorf("id must be a string")
    }
    
    // Int argument (may come as float64 from JSON)
    var limit int
    switch v := p.Args["limit"].(type) {
    case int:
        limit = v
    case float64:
        limit = int(v)
    default:
        return nil, fmt.Errorf("limit must be an integer")
    }
    
    // Optional argument with default
    offset := 0
    if val, ok := p.Args["offset"]; ok {
        offset = val.(int)
    }
    
    return fetchItems(id, limit, offset), nil
}

Concurrent Resolver Pattern

import "sync"

// Resolve multiple fields concurrently
resolve: func(p graphql.ResolveParams) (interface{}, error) {
    var wg sync.WaitGroup
    var user *User
    var posts []*Post
    var errUser, errPosts error
    
    wg.Add(2)
    
    go func() {
        defer wg.Done()
        user, errUser = fetchUser(id)
    }()
    
    go func() {
        defer wg.Done()
        posts, errPosts = fetchPosts(id)
    }()
    
    wg.Wait()
    
    if errUser != nil {
        return nil, errUser
    }
    if errPosts != nil {
        return nil, errPosts
    }
    
    return map[string]interface{}{
        "user":  user,
        "posts": posts,
    }, nil
}

Reflection-Based Resolvers

// Struct with exported fields - default resolver works automatically
type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

userType := graphql.NewObject(graphql.ObjectConfig{
    Name: "User",
    Fields: graphql.Fields{
        "id":    &graphql.Field{Type: graphql.ID},    // No Resolve needed
        "name":  &graphql.Field{Type: graphql.String}, // Default resolver uses reflection
        "email": &graphql.Field{Type: graphql.String},
    },
})

// Query returns *User - fields resolve automatically
resolve: func(p graphql.ResolveParams) (interface{}, error) {
    return &User{ID: "1", Name: "Alice", Email: "alice@example.com"}, nil
}

Edge Cases

Nil Handling

// Nullable field - can return nil
"optionalField": &graphql.Field{
    Type: graphql.String, // Nullable
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
        return nil, nil // OK - field will be null in response
    },
}

// Non-null field - nil causes error
"requiredField": &graphql.Field{
    Type: graphql.NewNonNull(graphql.String), // Non-null
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
        return nil, nil // ERROR - violates non-null constraint
    },
}

List Handling

// Empty list vs null
"items": &graphql.Field{
    Type: graphql.NewList(itemType), // [Item]
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
        return []interface{}{}, nil // Returns empty array []
        // return nil, nil           // Returns null
    },
}

// Non-null list that can contain nulls
Type: graphql.NewNonNull(graphql.NewList(itemType)) // [Item]!

// List of non-null items
Type: graphql.NewList(graphql.NewNonNull(itemType)) // [Item!]

// Non-null list of non-null items
Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(itemType))) // [Item!]!

Type Assertion Safety

resolve: func(p graphql.ResolveParams) (interface{}, error) {
    // Unsafe - panics if wrong type
    id := p.Args["id"].(string)
    
    // Safe - check before asserting
    idVal, ok := p.Args["id"].(string)
    if !ok {
        return nil, fmt.Errorf("id must be a string")
    }
    
    // Handle optional arguments
    var limit int = 10 // default
    if limitVal, ok := p.Args["limit"]; ok {
        limit, ok = limitVal.(int)
        if !ok {
            return nil, fmt.Errorf("limit must be an integer")
        }
    }
    
    return fetchData(idVal, limit), nil
}

Performance Considerations

Cache Parsed Documents

var documentCache = make(map[string]*ast.Document)
var cacheMu sync.RWMutex

func executeQuery(queryString string) *graphql.Result {
    cacheMu.RLock()
    doc, exists := documentCache[queryString]
    cacheMu.RUnlock()
    
    if !exists {
        var err error
        doc, err = parser.Parse(parser.ParseParams{Source: queryString})
        if err != nil {
            return &graphql.Result{Errors: []gqlerrors.FormattedError{{Message: err.Error()}}}
        }
        
        cacheMu.Lock()
        documentCache[queryString] = doc
        cacheMu.Unlock()
    }
    
    return graphql.Execute(graphql.ExecuteParams{
        Schema: schema,
        AST:    doc,
    })
}

Resolver Concurrency

// Resolvers can be called concurrently - ensure thread safety
type safeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *safeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

For more details, see the reference documentation.

Install with Tessl CLI

npx tessl i tessl/golang-github-com--graphql-go--graphql

docs

examples

real-world-scenarios.md

index.md

tile.json