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
- Learn about WebSockets
- Explore REST APIs
- Study Authentication