Create a GraphQL Server with Go and Google App Engine

Tin Rabzelj
Tin RabzeljHire me

October 2, 2017

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.

Go
GraphQL
Google App Engine

Newsletter

Get awesome articles delivered right to your doorstep

Protected by reCAPTCHA - Privacy - Terms

Related

Building a URL Shortener with Go and AWS Lambda

Building a Microservices Application in Go Following the CQRS Pattern

Building a Real-time Collaborative Drawing App in Go