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)

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 name , _ := params . Args [ "name" ] . ( string ) user := & User { Name : name } key := datastore . NewIncompleteKey ( ctx , "User" , nil ) if generatedKey , err := datastore . Put ( ctx , key , user ) ; err != nil { return User { } , err } else { 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 ) body , err := ioutil . ReadAll ( r . Body ) if err != nil { responseError ( w , "Invalid request body" , http . StatusBadRequest ) return } resp := graphql . Do ( graphql . Params { Schema : schema , RequestString : string ( body ) , Context : ctx , } ) if len ( resp . Errors ) > 0 { responseError ( w , fmt . Sprintf ( "%+v" , resp . Errors ) , http . StatusBadRequest ) return } 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 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 ) if generatedKey , err := datastore . Put ( ctx , key , post ) ; err != nil { return Post { } , err } else { 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 ) { query = query . Order ( "-CreatedAt" ) var result PostListResult if keys , err := query . GetAll ( ctx , & result . Nodes ) ; err != nil { return result , err } else { for i , key := range keys { result . Nodes [ i ] . ID = strconv . FormatInt ( key . IntID ( ) , 10 ) } 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 { 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 ) if err := datastore . Get ( ctx , key , user ) ; err != nil { 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 ) } 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.