Skip to main content
โšก Calmops

GraphQL with Go

GraphQL with Go

Introduction

GraphQL is a query language for APIs that provides a more flexible alternative to REST. This guide covers building GraphQL APIs in Go using gqlgen, the most popular GraphQL library for Go.

Core Concepts

GraphQL Basics

  • Schema: Defines types and queries
  • Queries: Read data
  • Mutations: Modify data
  • Resolvers: Functions that return data
  • Subscriptions: Real-time updates

Why GraphQL?

  • Clients request exactly what they need
  • Single endpoint for all operations
  • Strong typing with schema
  • Excellent developer experience

Good: GraphQL Schema

Defining Schema

# โœ… GOOD: GraphQL schema
type User {
  id: ID!
  name: String!
  email: String!
  age: Int
  posts: [Post!]!
}

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

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

type Mutation {
  createUser(name: String!, email: String!, age: Int): User!
  updateUser(id: ID!, name: String, email: String, age: Int): User
  deleteUser(id: ID!): Boolean!
  createPost(title: String!, content: String!, authorID: ID!): Post!
  updatePost(id: ID!, title: String, content: String): Post
  deletePost(id: ID!): Boolean!
}

Good: Implementing Resolvers

Query Resolvers

package main

import (
	"context"
	"github.com/99designs/gqlgen/graphql"
)

// โœ… GOOD: Query resolver
type QueryResolver struct {
	db *Database
}

func (r *QueryResolver) User(ctx context.Context, id string) (*User, error) {
	return r.db.GetUser(id)
}

func (r *QueryResolver) Users(ctx context.Context, limit *int, offset *int) ([]*User, error) {
	l := 10
	if limit != nil {
		l = *limit
	}

	o := 0
	if offset != nil {
		o = *offset
	}

	return r.db.GetUsers(l, o)
}

func (r *QueryResolver) Post(ctx context.Context, id string) (*Post, error) {
	return r.db.GetPost(id)
}

func (r *QueryResolver) Posts(ctx context.Context, limit *int, offset *int) ([]*Post, error) {
	l := 10
	if limit != nil {
		l = *limit
	}

	o := 0
	if offset != nil {
		o = *offset
	}

	return r.db.GetPosts(l, o)
}

Mutation Resolvers

package main

import (
	"context"
)

// โœ… GOOD: Mutation resolver
type MutationResolver struct {
	db *Database
}

func (r *MutationResolver) CreateUser(ctx context.Context, name string, email string, age *int) (*User, error) {
	user := &User{
		Name:  name,
		Email: email,
	}

	if age != nil {
		user.Age = *age
	}

	return r.db.CreateUser(user)
}

func (r *MutationResolver) UpdateUser(ctx context.Context, id string, name *string, email *string, age *int) (*User, error) {
	user, err := r.db.GetUser(id)
	if err != nil {
		return nil, err
	}

	if name != nil {
		user.Name = *name
	}
	if email != nil {
		user.Email = *email
	}
	if age != nil {
		user.Age = *age
	}

	return r.db.UpdateUser(user)
}

func (r *MutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
	return r.db.DeleteUser(id)
}

func (r *MutationResolver) CreatePost(ctx context.Context, title string, content string, authorID string) (*Post, error) {
	post := &Post{
		Title:    title,
		Content:  content,
		AuthorID: authorID,
	}

	return r.db.CreatePost(post)
}

func (r *MutationResolver) UpdatePost(ctx context.Context, id string, title *string, content *string) (*Post, error) {
	post, err := r.db.GetPost(id)
	if err != nil {
		return nil, err
	}

	if title != nil {
		post.Title = *title
	}
	if content != nil {
		post.Content = *content
	}

	return r.db.UpdatePost(post)
}

func (r *MutationResolver) DeletePost(ctx context.Context, id string) (bool, error) {
	return r.db.DeletePost(id)
}

Field Resolvers

package main

import (
	"context"
)

// โœ… GOOD: Field resolver for nested data
type UserResolver struct {
	db *Database
}

func (r *UserResolver) Posts(ctx context.Context, obj *User) ([]*Post, error) {
	return r.db.GetUserPosts(obj.ID)
}

type PostResolver struct {
	db *Database
}

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

Advanced Patterns

Middleware and Context

package main

import (
	"context"
	"github.com/99designs/gqlgen/graphql"
)

// โœ… GOOD: Authentication middleware
func authMiddleware(next graphql.Handler) graphql.Handler {
	return graphql.HandlerFunc(func(ctx context.Context, w graphql.ResponseWriter, r *graphql.Request) {
		// Check authentication
		userID := r.Header.Get("Authorization")
		if userID == "" {
			w.WriteError(graphql.NewError("Unauthorized"))
			return
		}

		// Add to context
		ctx = context.WithValue(ctx, "userID", userID)
		next.ServeGraphQL(ctx, w, r)
	})
}

// โœ… GOOD: Get user from context
func getUserFromContext(ctx context.Context) (string, error) {
	userID, ok := ctx.Value("userID").(string)
	if !ok {
		return "", graphql.NewError("Unauthorized")
	}
	return userID, nil
}

Error Handling

package main

import (
	"github.com/99designs/gqlgen/graphql"
)

// โœ… GOOD: Custom error handler
func errorPresenter(ctx context.Context, err error) *graphql.Error {
	return &graphql.Error{
		Message: err.Error(),
		Extensions: map[string]interface{}{
			"code": "INTERNAL_ERROR",
		},
	}
}

Best Practices

1. Use Proper Naming

# โœ… GOOD: Clear, descriptive names
type User {
  id: ID!
  firstName: String!
  lastName: String!
  emailAddress: String!
}

# โŒ BAD: Unclear names
type User {
  id: ID!
  fn: String!
  ln: String!
  em: String!
}

2. Use Proper Types

# โœ… GOOD: Appropriate types
type User {
  id: ID!
  name: String!
  age: Int
  email: String!
  createdAt: String!
}

# โŒ BAD: Everything as string
type User {
  id: String!
  name: String!
  age: String!
  email: String!
  createdAt: String!
}

3. Implement Pagination

# โœ… GOOD: Pagination
type Query {
  users(first: Int, after: String): UserConnection!
}

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

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

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

Resources

Summary

GraphQL provides a flexible, type-safe way to build APIs. Use gqlgen to generate resolvers from schema, implement proper error handling, and follow GraphQL best practices. GraphQL’s strong typing and query flexibility make it ideal for complex API requirements.

Comments