1. go
  2. /web
  3. /graphql

Implementing GraphQL in Go

GraphQL provides a flexible way to query and mutate data. This guide covers how to implement GraphQL APIs in Go using gqlgen.

Basic Setup

Schema Definition

# schema.graphql
type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
}

type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
}

type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
    post(id: ID!): Post
}

type Mutation {
    createUser(input: CreateUserInput!): User!
    createPost(input: CreatePostInput!): Post!
}

input CreateUserInput {
    name: String!
    email: String!
}

input CreatePostInput {
    title: String!
    content: String!
    authorId: ID!
}

Server Setup

package main

import (
    "log"
    "net/http"
    
    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/playground"
)

func main() {
    srv := handler.NewDefaultServer(
        generated.NewExecutableSchema(generated.Config{
            Resolvers: &graph.Resolver{},
        }),
    )
    
    http.Handle("/", playground.Handler("GraphQL", "/query"))
    http.Handle("/query", srv)
    
    log.Printf("Server is running on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Resolvers

Query Resolvers

type queryResolver struct {
    *Resolver
}

func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
    return r.DB.GetUsers()
}

func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
    return r.DB.GetUser(id)
}

func (r *queryResolver) Posts(ctx context.Context) ([]*model.Post, error) {
    return r.DB.GetPosts()
}

func (r *queryResolver) Post(ctx context.Context, id string) (*model.Post, error) {
    return r.DB.GetPost(id)
}

Mutation Resolvers

type mutationResolver struct {
    *Resolver
}

func (r *mutationResolver) CreateUser(ctx context.Context, input model.CreateUserInput) (*model.User, error) {
    user := &model.User{
        ID:    generateID(),
        Name:  input.Name,
        Email: input.Email,
    }
    
    if err := r.DB.CreateUser(user); err != nil {
        return nil, err
    }
    
    return user, nil
}

func (r *mutationResolver) CreatePost(ctx context.Context, input model.CreatePostInput) (*model.Post, error) {
    post := &model.Post{
        ID:       generateID(),
        Title:    input.Title,
        Content:  input.Content,
        AuthorID: input.AuthorID,
    }
    
    if err := r.DB.CreatePost(post); err != nil {
        return nil, err
    }
    
    return post, nil
}

Field Resolvers

Relationship Resolution

type userResolver struct {
    *Resolver
}

func (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) {
    return r.DB.GetPostsByAuthor(obj.ID)
}

type postResolver struct {
    *Resolver
}

func (r *postResolver) Author(ctx context.Context, obj *model.Post) (*model.User, error) {
    return r.DB.GetUser(obj.AuthorID)
}

Batch Loading

type batchUserLoader struct {
    *dataloader.Loader
}

func newBatchUserLoader(db *DB) *batchUserLoader {
    return &batchUserLoader{
        dataloader.NewBatchedLoader(func(ctx context.Context, keys []string) []*dataloader.Result {
            users, err := db.GetUsersByIDs(keys)
            if err != nil {
                return handleError(err, len(keys))
            }
            
            // Map users by ID
            userMap := make(map[string]*model.User)
            for _, user := range users {
                userMap[user.ID] = user
            }
            
            // Build results in order
            results := make([]*dataloader.Result, len(keys))
            for i, key := range keys {
                user, ok := userMap[key]
                if !ok {
                    results[i] = &dataloader.Result{
                        Error: fmt.Errorf("user not found: %s", key),
                    }
                    continue
                }
                results[i] = &dataloader.Result{Data: user}
            }
            
            return results
        }),
    }
}

Middleware

Authentication

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            next.ServeHTTP(w, r)
            return
        }
        
        // Validate token
        user, err := validateToken(token)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        
        // Add user to context
        ctx := context.WithValue(r.Context(), "user", user)
        r = r.WithContext(ctx)
        
        next.ServeHTTP(w, r)
    })
}

Logging

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Create response recorder
        recorder := &responseRecorder{
            ResponseWriter: w,
            status:        http.StatusOK,
        }
        
        // Process request
        next.ServeHTTP(recorder, r)
        
        // Log request details
        log.Printf(
            "GraphQL request: %s %s %d %v",
            r.Method,
            r.URL.Path,
            recorder.status,
            time.Since(start),
        )
    })
}

Best Practices

1. Schema Organization

# types/user.graphql
type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
}

extend type Query {
    users: [User!]!
    user(id: ID!): User
}

extend type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
    deleteUser(id: ID!): Boolean!
}

input CreateUserInput {
    name: String!
    email: String!
}

input UpdateUserInput {
    name: String
    email: String
}

# types/post.graphql
type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
}

extend type Query {
    posts: [Post!]!
    post(id: ID!): Post
}

extend type Mutation {
    createPost(input: CreatePostInput!): Post!
    updatePost(id: ID!, input: UpdatePostInput!): Post!
    deletePost(id: ID!): Boolean!
}

2. Error Handling

type GraphQLError struct {
    Message string                 `json:"message"`
    Code    string                 `json:"code"`
    Data    map[string]interface{} `json:"data,omitempty"`
}

func (e *GraphQLError) Error() string {
    return e.Message
}

func HandleError(ctx context.Context, err error) error {
    switch e := err.(type) {
    case *GraphQLError:
        return e
    case *ValidationError:
        return &GraphQLError{
            Message: "Validation error",
            Code:    "VALIDATION_ERROR",
            Data: map[string]interface{}{
                "fields": e.Fields,
            },
        }
    default:
        return &GraphQLError{
            Message: "Internal server error",
            Code:    "INTERNAL_ERROR",
        }
    }
}

3. Authorization

func (r *queryResolver) requireAuth(ctx context.Context) (*model.User, error) {
    user := ctx.Value("user")
    if user == nil {
        return nil, &GraphQLError{
            Message: "Unauthorized",
            Code:    "UNAUTHORIZED",
        }
    }
    return user.(*model.User), nil
}

func (r *queryResolver) Posts(ctx context.Context) ([]*model.Post, error) {
    user, err := r.requireAuth(ctx)
    if err != nil {
        return nil, err
    }
    
    // Get posts with user context
    return r.DB.GetPostsByUser(user.ID)
}

Common Patterns

1. Pagination

type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
}

type UserConnection {
    edges: [UserEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
}

type UserEdge {
    node: User!
    cursor: String!
}

extend type Query {
    users(
        first: Int
        after: String
        last: Int
        before: String
    ): UserConnection!
}
func (r *queryResolver) Users(ctx context.Context, first *int, after *string, last *int, before *string) (*model.UserConnection, error) {
    params := &PaginationParams{
        First:  first,
        After:  after,
        Last:   last,
        Before: before,
    }
    
    // Get paginated users
    users, pageInfo, total, err := r.DB.GetPaginatedUsers(params)
    if err != nil {
        return nil, err
    }
    
    // Build edges
    edges := make([]*model.UserEdge, len(users))
    for i, user := range users {
        edges[i] = &model.UserEdge{
            Node:   user,
            Cursor: encodeCursor(user.ID),
        }
    }
    
    return &model.UserConnection{
        Edges:      edges,
        PageInfo:   pageInfo,
        TotalCount: total,
    }, nil
}

2. Subscriptions

type Subscription {
    userCreated: User!
    postCreated(userId: ID): Post!
}
type subscriptionResolver struct {
    *Resolver
}

func (r *subscriptionResolver) UserCreated(ctx context.Context) (<-chan *model.User, error) {
    ch := make(chan *model.User)
    
    go func() {
        defer close(ch)
        
        // Subscribe to user events
        sub := r.PubSub.Subscribe("user:created")
        defer sub.Close()
        
        for {
            select {
            case <-ctx.Done():
                return
            case msg := <-sub.Channel():
                var user model.User
                if err := json.Unmarshal([]byte(msg.Payload), &user); err != nil {
                    log.Printf("Error unmarshaling user: %v", err)
                    continue
                }
                ch <- &user
            }
        }
    }()
    
    return ch, nil
}

Next Steps