GraphQL

GraphQL adalah query language untuk API yang memberikan client kemampuan untuk meminta data yang spesifik. Go memiliki beberapa package untuk implementasi GraphQL server.

Contoh Masalah

Bagaimana cara:

  1. Setup GraphQL server
  2. Define schema
  3. Implement resolvers
  4. Handle mutations

Penyelesaian

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"

    "github.com/graphql-go/graphql"
)

// 1. Data types
type User struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"createdAt"`
    Posts     []Post    `json:"posts"`
}

type Post struct {
    ID        string    `json:"id"`
    Title     string    `json:"title"`
    Content   string    `json:"content"`
    AuthorID  string    `json:"authorId"`
    CreatedAt time.Time `json:"createdAt"`
    Comments  []Comment `json:"comments"`
}

type Comment struct {
    ID        string    `json:"id"`
    Content   string    `json:"content"`
    PostID    string    `json:"postId"`
    AuthorID  string    `json:"authorId"`
    CreatedAt time.Time `json:"createdAt"`
}

// 2. In-memory database
type DB struct {
    sync.RWMutex
    users    map[string]User
    posts    map[string]Post
    comments map[string]Comment
}

func NewDB() *DB {
    return &DB{
        users:    make(map[string]User),
        posts:    make(map[string]Post),
        comments: make(map[string]Comment),
    }
}

// 3. GraphQL types
var userType = graphql.NewObject(
    graphql.ObjectConfig{
        Name: "User",
        Fields: graphql.Fields{
            "id": &graphql.Field{
                Type: graphql.String,
            },
            "name": &graphql.Field{
                Type: graphql.String,
            },
            "email": &graphql.Field{
                Type: graphql.String,
            },
            "createdAt": &graphql.Field{
                Type: graphql.DateTime,
            },
            "posts": &graphql.Field{
                Type: graphql.NewList(postType),
                Resolve: func(p graphql.ResolveParams) (interface{},
                    error) {
                    db := p.Context.Value("db").(*DB)
                    user := p.Source.(User)
                    
                    var userPosts []Post
                    for _, post := range db.posts {
                        if post.AuthorID == user.ID {
                            userPosts = append(userPosts, post)
                        }
                    }
                    return userPosts, nil
                },
            },
        },
    },
)

var postType = graphql.NewObject(
    graphql.ObjectConfig{
        Name: "Post",
        Fields: graphql.Fields{
            "id": &graphql.Field{
                Type: graphql.String,
            },
            "title": &graphql.Field{
                Type: graphql.String,
            },
            "content": &graphql.Field{
                Type: graphql.String,
            },
            "authorId": &graphql.Field{
                Type: graphql.String,
            },
            "createdAt": &graphql.Field{
                Type: graphql.DateTime,
            },
            "author": &graphql.Field{
                Type: userType,
                Resolve: func(p graphql.ResolveParams) (interface{},
                    error) {
                    db := p.Context.Value("db").(*DB)
                    post := p.Source.(Post)
                    user, exists := db.users[post.AuthorID]
                    if !exists {
                        return nil, fmt.Errorf("author not found")
                    }
                    return user, nil
                },
            },
            "comments": &graphql.Field{
                Type: graphql.NewList(commentType),
                Resolve: func(p graphql.ResolveParams) (interface{},
                    error) {
                    db := p.Context.Value("db").(*DB)
                    post := p.Source.(Post)
                    
                    var postComments []Comment
                    for _, comment := range db.comments {
                        if comment.PostID == post.ID {
                            postComments = append(postComments,
                                comment)
                        }
                    }
                    return postComments, nil
                },
            },
        },
    },
)

var commentType = graphql.NewObject(
    graphql.ObjectConfig{
        Name: "Comment",
        Fields: graphql.Fields{
            "id": &graphql.Field{
                Type: graphql.String,
            },
            "content": &graphql.Field{
                Type: graphql.String,
            },
            "postId": &graphql.Field{
                Type: graphql.String,
            },
            "authorId": &graphql.Field{
                Type: graphql.String,
            },
            "createdAt": &graphql.Field{
                Type: graphql.DateTime,
            },
            "author": &graphql.Field{
                Type: userType,
                Resolve: func(p graphql.ResolveParams) (interface{},
                    error) {
                    db := p.Context.Value("db").(*DB)
                    comment := p.Source.(Comment)
                    user, exists := db.users[comment.AuthorID]
                    if !exists {
                        return nil, fmt.Errorf("author not found")
                    }
                    return user, nil
                },
            },
            "post": &graphql.Field{
                Type: postType,
                Resolve: func(p graphql.ResolveParams) (interface{},
                    error) {
                    db := p.Context.Value("db").(*DB)
                    comment := p.Source.(Comment)
                    post, exists := db.posts[comment.PostID]
                    if !exists {
                        return nil, fmt.Errorf("post not found")
                    }
                    return post, nil
                },
            },
        },
    },
)

// 4. Query type
var 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.String),
                    },
                },
                Resolve: func(p graphql.ResolveParams) (interface{},
                    error) {
                    db := p.Context.Value("db").(*DB)
                    id := p.Args["id"].(string)
                    user, exists := db.users[id]
                    if !exists {
                        return nil, fmt.Errorf("user not found")
                    }
                    return user, nil
                },
            },
            "users": &graphql.Field{
                Type: graphql.NewList(userType),
                Resolve: func(p graphql.ResolveParams) (interface{},
                    error) {
                    db := p.Context.Value("db").(*DB)
                    var users []User
                    for _, user := range db.users {
                        users = append(users, user)
                    }
                    return users, nil
                },
            },
            "post": &graphql.Field{
                Type: postType,
                Args: graphql.FieldConfigArgument{
                    "id": &graphql.ArgumentConfig{
                        Type: graphql.NewNonNull(graphql.String),
                    },
                },
                Resolve: func(p graphql.ResolveParams) (interface{},
                    error) {
                    db := p.Context.Value("db").(*DB)
                    id := p.Args["id"].(string)
                    post, exists := db.posts[id]
                    if !exists {
                        return nil, fmt.Errorf("post not found")
                    }
                    return post, nil
                },
            },
        },
    },
)

// 5. Mutation type
var 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) {
                    db := p.Context.Value("db").(*DB)
                    name := p.Args["name"].(string)
                    email := p.Args["email"].(string)

                    user := User{
                        ID:        fmt.Sprintf("user_%d",
                            time.Now().UnixNano()),
                        Name:      name,
                        Email:     email,
                        CreatedAt: time.Now(),
                    }

                    db.Lock()
                    db.users[user.ID] = user
                    db.Unlock()

                    return user, nil
                },
            },
            "createPost": &graphql.Field{
                Type: postType,
                Args: graphql.FieldConfigArgument{
                    "title": &graphql.ArgumentConfig{
                        Type: graphql.NewNonNull(graphql.String),
                    },
                    "content": &graphql.ArgumentConfig{
                        Type: graphql.NewNonNull(graphql.String),
                    },
                    "authorId": &graphql.ArgumentConfig{
                        Type: graphql.NewNonNull(graphql.String),
                    },
                },
                Resolve: func(p graphql.ResolveParams) (interface{},
                    error) {
                    db := p.Context.Value("db").(*DB)
                    title := p.Args["title"].(string)
                    content := p.Args["content"].(string)
                    authorID := p.Args["authorId"].(string)

                    if _, exists := db.users[authorID]; !exists {
                        return nil, fmt.Errorf("author not found")
                    }

                    post := Post{
                        ID:        fmt.Sprintf("post_%d",
                            time.Now().UnixNano()),
                        Title:     title,
                        Content:   content,
                        AuthorID:  authorID,
                        CreatedAt: time.Now(),
                    }

                    db.Lock()
                    db.posts[post.ID] = post
                    db.Unlock()

                    return post, nil
                },
            },
        },
    },
)

// 6. Schema
var schema, _ = graphql.NewSchema(
    graphql.SchemaConfig{
        Query:    queryType,
        Mutation: mutationType,
    },
)

// 7. HTTP handler
func graphqlHandler(db *DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var params struct {
            Query         string                 `json:"query"`
            OperationName string                 `json:"operationName"`
            Variables     map[string]interface{} `json:"variables"`
        }

        if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }

        ctx := context.WithValue(r.Context(), "db", db)
        result := graphql.Do(graphql.Params{
            Schema:         schema,
            RequestString: params.Query,
            Context:       ctx,
            OperationName: params.OperationName,
            Variables:     params.Variables,
        })

        if len(result.Errors) > 0 {
            w.WriteHeader(http.StatusBadRequest)
        }

        json.NewEncoder(w).Encode(result)
    }
}

func main() {
    // Create database
    db := NewDB()

    // Add sample data
    user := User{
        ID:        "user_1",
        Name:      "John Doe",
        Email:     "john@example.com",
        CreatedAt: time.Now(),
    }
    db.users[user.ID] = user

    post := Post{
        ID:        "post_1",
        Title:     "Hello GraphQL",
        Content:   "This is my first post",
        AuthorID:  user.ID,
        CreatedAt: time.Now(),
    }
    db.posts[post.ID] = post

    // Setup HTTP server
    http.HandleFunc("/graphql", graphqlHandler(db))

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Example Queries

  1. Get user with posts:
{
  user(id: "user_1") {
    id
    name
    email
    posts {
      id
      title
      content
    }
  }
}
  1. Create user:
mutation {
  createUser(name: "Jane Doe", email: "jane@example.com") {
    id
    name
    email
    createdAt
  }
}
  1. Create post:
mutation {
  createPost(
    title: "My Post"
    content: "Post content"
    authorId: "user_1"
  ) {
    id
    title
    content
    author {
      name
    }
  }
}

Penjelasan Kode

  1. GraphQL Components

    • Schema
    • Types
    • Resolvers
    • Context
  2. Features

    • Queries
    • Mutations
    • Nested resolvers
    • Arguments
  3. Best Practices

    • Error handling
    • Context usage
    • Type definitions

Setup

  1. Install dependencies:
go get github.com/graphql-go/graphql
  1. Run server:
go run main.go
  1. Test queries:
curl -X POST http://localhost:8080/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ user(id: \"user_1\") { name email } }"}'

Output

2024/01/21 11:57:22 Server starting on :8080

Query response:
{
  "data": {
    "user": {
      "name": "John Doe",
      "email": "john@example.com"
    }
  }
}

Mutation response:
{
  "data": {
    "createUser": {
      "id": "user_1234567890",
      "name": "Jane Doe",
      "email": "jane@example.com",
      "createdAt": "2024-01-21T11:57:23Z"
    }
  }
}

Tips

  • Define clear types
  • Use context
  • Handle errors
  • Implement validation
  • Monitor performance