Create a GraphQL Server with Go and Google App Engine

Tin Rabzelj
02 October, 2017

cover

GraphQL is better than REST for developing and consuming APIs. It lets you query the exact data you need without having to create many overspecific API endpoints. This article describes how to get GraphQL server running on Google App Engine with a "simple social network" data model.

This tutorial uses the following technologies:

  • Google Cloud SDK 173.0.0 (Installation instructions, also install google-cloud-sdk-app-engine-go component)
  • Go 1.8

Getting started

Inside project's directory create the app.yaml file.

runtime: go
api_version: go1.8

handlers:
- url: /.*
  script: _go_app

Create an entry point file app.go.

package app

func init() {
}

Run the development server and keep it running in the background.

$ dev_appserver.py .

Write some utility functions inside utilities.go file.

package app

import (
	"encoding/json"
	"net/http"
)

func responseError(w http.ResponseWriter, message string, code int) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(map[string]string{"error": message})
}

func responseJSON(w http.ResponseWriter, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(data)
}

Declare structs which represent your data model in models.go. This example application has multiple users, each having their own posts.

package app

import "time"

type User struct {
	ID   string `json:"id" datastore:"-"`
	Name string `json:"name"`
}

type Post struct {
	ID        string    `json:"id" datastore:"-"`
	UserID    string    `json:"userId"`
	CreatedAt time.Time `json:"createdAt"`
	Content   string    `json:"content"`
}

Mutations

First off, install graphql-go/graphql package to work with GraphQL.

$ go get github.com/graphql-go/graphql

Creating users

Inside app.go declare the user type and root mutation containing createUser field.

var schema graphql.Schema
var userType = graphql.NewObject(graphql.ObjectConfig{
	Name: "User",
	Fields: graphql.Fields{
		"id":    &graphql.Field{Type: graphql.String},
		"name":  &graphql.Field{Type: graphql.String},
	},
})
var rootMutation = graphql.NewObject(graphql.ObjectConfig{
	Name: "RootMutation",
	Fields: graphql.Fields{
		"createUser": &graphql.Field{
			Type: userType,
			Args: graphql.FieldConfigArgument{
				"name": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
			},
			Resolve: createUser,
		},
	},
})

All resolver functions will be kept inside resolvers.go file. Write the createUser function.

func createUser(params graphql.ResolveParams) (interface{}, error) {
	ctx := params.Context
  // Get the name argument
	name, _ := params.Args["name"].(string)
	user := &User{Name: name}
	key := datastore.NewIncompleteKey(ctx, "User", nil)
	// Insert user into Datastore
  if generatedKey, err := datastore.Put(ctx, key, user); err != nil {
		return User{}, err
	} else {
    // Set user's auto-generated ID
		user.ID = strconv.FormatInt(generatedKey.IntID(), 10)
	}
	return user, nil
}

Inside the init function build the schema and hook up a HTTP handler, which reads GraphQL query from the request body.

func init() {
	schema, _ = graphql.NewSchema(graphql.SchemaConfig{
		Mutation: rootMutation,
	})
	http.HandleFunc("/", handler)
}
func handler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)
  // Read the query
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		responseError(w, "Invalid request body", http.StatusBadRequest)
		return
	}
  // Perform GraphQL request
	resp := graphql.Do(graphql.Params{
		Schema:        schema,
		RequestString: string(body),
		Context:       ctx,
	})
  // Check for errors
	if len(resp.Errors) > 0 {
		responseError(w, fmt.Sprintf("%+v", resp.Errors), http.StatusBadRequest)
		return
	}
  // Return the result
	responseJSON(w, resp)
}

Now you should be able to create a few users with this mutation.

mutation {
  john: createUser(name: "John") {
    id
  },
  bob: createUser(name: "Bob") {
    id
  },
  mark: createUser(name: "Mark") {
    id
  }
}

Run it using cURL.

$ curl localhost:8080 -d 'mutation{john:createUser(name:"John"){id},bob:createUser(name:"Bob"){id},mark:createUser(name:"Mark"){id}}'
{
  "data": {
    "bob": {
      "id": "5205088045891584"
    },
    "john": {
      "id": "5768037999312896"
    },
    "mark": {
      "id": "6330987952734208"
    }
  }
}

Creating posts

Declare the post type.

var postType = graphql.NewObject(graphql.ObjectConfig{
	Name: "Post",
	Fields: graphql.Fields{
		"id":        &graphql.Field{Type: graphql.String},
		"userId":    &graphql.Field{Type: graphql.String},
		"createdAt": &graphql.Field{Type: graphql.DateTime},
		"content":   &graphql.Field{Type: graphql.String},
	},
})

Update the rootMutation.

var rootMutation = graphql.NewObject(graphql.ObjectConfig{
	Name: "RootMutation",
	Fields: graphql.Fields{
		"createUser": &graphql.Field{
			Type: userType,
			Args: graphql.FieldConfigArgument{
				"name": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
			},
			Resolve: createUser,
		},
		"createPost": &graphql.Field{
			Type: postType,
			Args: graphql.FieldConfigArgument{
				"userId":  &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
				"content": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
			},
			Resolve: createPost,
		},
	},
})

Write the createPost function. Note that validity of userId argument is not checked in this example.

func createPost(params graphql.ResolveParams) (interface{}, error) {
	ctx := params.Context
  // Get arguments
	content, _ := params.Args["content"].(string)
	userID, _ := params.Args["userId"].(string)
	post := &Post{UserID: userID, Content: content, CreatedAt: time.Now().UTC()}
	key := datastore.NewIncompleteKey(ctx, "Post", nil)
	// Insert post
  if generatedKey, err := datastore.Put(ctx, key, post); err != nil {
		return Post{}, err
	} else {
    // Update post's ID
		post.ID = strconv.FormatInt(generatedKey.IntID(), 10)
	}
	return post, nil
}

Create a few posts for one of the existing users.

$ curl localhost:8080 -d 'mutation{a:createPost(userId:"5768037999312896",content:"Hi!"){id,content},b:createPost(userId:"5768037999312896",content:"lol"){id,content},c:createPost(userId:"5768037999312896",content:"GraphQL is pretty cool!"){id,content}}'
{
  "data": {
    "a": {
      "content": "Hi!",
      "id": "4923613069180928"
    },
    "b": {
      "content": "lol",
      "id": "6049512976023552"
    },
    "c": {
      "content": "GraphQL is pretty cool!",
      "id": "5486563022602240"
    }
  }
}

Queries

When working with lists, you normally want API to provide a way to paginate resulting objects. To accomplish this, two optional values will be passed in as arguments—limit and offset. Response for lists will contain nodes and a total count. Write utility functions for creating a list field and a list type.

func makeListField(listType graphql.Output, resolve graphql.FieldResolveFn) *graphql.Field {
	return &graphql.Field{
		Type:    listType,
		Resolve: resolve,
		Args: graphql.FieldConfigArgument{
			"limit":  &graphql.ArgumentConfig{Type: graphql.Int},
			"offset": &graphql.ArgumentConfig{Type: graphql.Int},
		},
	}
}

func makeNodeListType(name string, nodeType *graphql.Object) *graphql.Object {
	return graphql.NewObject(graphql.ObjectConfig{
		Name: name,
		Fields: graphql.Fields{
			"nodes":      &graphql.Field{Type: graphql.NewList(nodeType)},
			"totalCount": &graphql.Field{Type: graphql.Int},
		},
	})
}

Query posts

Define the root query object with a posts field.

var rootQuery = graphql.NewObject(graphql.ObjectConfig{
	Name: "RootQuery",
	Fields: graphql.Fields{
		"posts": makeListField(makeNodeListType("PostList", postType), queryPosts),
	},
})

Update the init function.

func init() {
	schema, _ = graphql.NewSchema(graphql.SchemaConfig{
		Mutation: rootMutation,
	  Query:    rootQuery,
	})
	http.HandleFunc("/", handler)
}

Inside resolvers.go write the queryPostList function which runs provided query and returns PostListResult.

type PostListResult struct {
	Nodes      []Post `json:"nodes"`
	TotalCount int    `json:"totalCount"`
}

func queryPostList(ctx context.Context, query *datastore.Query) (PostListResult, error) {
  // Order by creation time
  query = query.Order("-CreatedAt")
	var result PostListResult
  // Run the query
	if keys, err := query.GetAll(ctx, &result.Nodes); err != nil {
		return result, err
	} else {
    // Set IDs
		for i, key := range keys {
			result.Nodes[i].ID = strconv.FormatInt(key.IntID(), 10)
		}
    // Set total count
		result.TotalCount = len(result.Nodes)
	}
	return result, nil
}

Write the queryPosts resolve function.

func queryPosts(params graphql.ResolveParams) (interface{}, error) {
	ctx := params.Context
	query := datastore.NewQuery("Post")
	if limit, ok := params.Args["limit"].(int); ok {
		query = query.Limit(limit)
	}
	if offset, ok := params.Args["offset"].(int); ok {
		query = query.Offset(offset)
	}
	return queryPostList(ctx, query)
}

You could pass in more arguments alongside limit and offset. For example, a filter argument and use it with Query.Filter.

Test it out.

$ curl localhost:8080 -d '{posts{totalCount,nodes{id,content,createdAt}}}'
{
  "data": {
    "posts": {
      "nodes": [{
          "content": "GraphQL is pretty cool!",
          "createdAt": "2017-10-02T17:04:43.359251Z",
          "id": "5486563022602240"
        },
        {
          "content": "lol",
          "createdAt": "2017-10-02T17:04:43.356026Z",
          "id": "6049512976023552"
        },
        {
          "content": "Hi!",
          "createdAt": "2017-10-02T17:04:43.350061Z",
          "id": "4923613069180928"
        }
      ],
      "totalCount": 3
    }
  }
}

Try with limit and offset.

$ curl localhost:8080 -d '{posts(limit:1,offset:1){totalCount,nodes{id,content,createdAt}}}'
{
  "data": {
    "posts": {
      "nodes": [{
        "content": "lol",
        "createdAt": "2017-10-02T17:04:43.356026Z",
        "id": "6049512976023552"
      }],
      "totalCount": 1
    }
  }
}

Query user

To fetch a user, you must perform a user query (queryUser) and then a nested query (queryPostsByUser) to get all posts by this user.

Update the user type.

var userType = graphql.NewObject(graphql.ObjectConfig{
	Name: "User",
	Fields: graphql.Fields{
		"id":    &graphql.Field{Type: graphql.String},
		"name":  &graphql.Field{Type: graphql.String},
		"posts": makeListField(makeNodeListType("PostList", postType), queryPostsByUser),
	},
})

Update the root query.

var rootQuery = graphql.NewObject(graphql.ObjectConfig{
	Name: "RootQuery",
	Fields: graphql.Fields{
		"user": &graphql.Field{
			Type: userType,
			Args: graphql.FieldConfigArgument{
				"id": &graphql.ArgumentConfig{Type: graphql.NewNonNull(graphql.String)},
			},
			Resolve: queryUser,
		},
		"posts": makeListField(makeNodeListType("PostList", postType), queryPosts),
	},
})

Write the queryUser resolve function inside resolvers.go.

func queryUser(params graphql.ResolveParams) (interface{}, error) {
	ctx := params.Context
	if strID, ok := params.Args["id"].(string); ok {
		// Parse ID argument
    id, err := strconv.ParseInt(strID, 10, 64)
		if err != nil {
			return nil, errors.New("Invalid id")
		}
		user := &User{ID: strID}
		key := datastore.NewKey(ctx, "User", "", id, nil)
		// Fetch user by ID
    if err := datastore.Get(ctx, key, user); err != nil {
      // Assume not found
			return nil, errors.New("User not found")
		}
		return user, nil
	}
	return User{}, nil
}

Write the queryPostsByUser resolve function. It's similar to queryPosts.

func queryPostsByUser(params graphql.ResolveParams) (interface{}, error) {
	ctx := params.Context
	query := datastore.NewQuery("Post")
	if limit, ok := params.Args["limit"].(int); ok {
		query = query.Limit(limit)
	}
	if offset, ok := params.Args["offset"].(int); ok {
		query = query.Offset(offset)
	}
  // Check user's ID against post's UserID field
	if user, ok := params.Source.(*User); ok {
		query = query.Filter("UserID =", user.ID)
	}
	return queryPostList(ctx, query)
}

Fetch posts of one of the users.

$ curl localhost:8080 -d '{user(id:"5768037999312896"){name,posts{totalCount,nodes{content}}}}'
{
  "data": {
    "user": {
      "name": "John",
      "posts": {
        "nodes": [{
            "content": "GraphQL is pretty cool!"
          },
          {
            "content": "lol"
          },
          {
            "content": "Hi!"
          }
        ],
        "totalCount": 3
      }
    }
  }
}

Wrapping up

This was a short introduction of using GraphQL with Google App Engine. Entire source code is available on GitHub.

Newsletter

Get awesome articles delivered right to your doorstep

Protected by reCAPTCHA - Privacy - Terms

Comments

Sign in to post a comment