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
- gqlgen: https://gqlgen.com/
- GraphQL: https://graphql.org/
- GraphQL Best Practices: https://graphql.org/learn/best-practices/
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