Create a GraphQL Server with Go and Google App Engine
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.