Complete GraphQL implementation in Go with schema definition, query execution, and validation.
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 } }`,
})
}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
}
}),
})// 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
},
})// 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 } } }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},
}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
},
},
}// 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})// 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"}
}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)
}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
}// 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
},
},
}// 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
}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,
})// 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// 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
}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
}// 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
}// 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
},
}// 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!]!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
}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,
})
}// 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--graphqldocs