Creating a Basic GraphQL API with Go

It seems like GraphQL is being used more and more and having been present at some of the first public talks about it within the react ecosystem a couple of years back, I’m not surprised.

The concept works out from several perspectives, with graph-like data, multiple distributed teams and highly versioned APIs as well as regarding type safety and documentation. GraphQL looks like a good fit for many different applications.

This post’s goal is not to introduce you the basics of GraphQL, but rather to see it in action in a realistic scenario. When planning to move an existing REST API to GraphQL, it makes sense to first introduce a translation-layer, to make a smooth transition.

In this post, we will use jsonplaceholder as the API we will wrap with GraphQL. There are a couple of libraries for graphQL in Go and for this example, graphql-go and graphql-go-handler will be used.

Our goal is to fetch posts and comments from jsonplaceholder and end up with a way to fetch posts by ID and, if the API consumer wishes to also fetch the comments, to nest the comments into the post via GraphQL.

Let’s get started.

Implementation

First, we define data models for Post and Comment:

type Post struct {
    UserID int    `json:"userId"`
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Body   string `json:"body"`
}

type Comment struct {
    PostID int    `json:"postId"`
    ID     int    `json:"id"`
    Name   string `json:"name"`
    Email  string `json:"email"`
    Body   string `json:"body"`
}

We also define a fetchPostByiD(id) function, which calls http://jsonplaceholder.typicode.com/posts/${id} and transforms the resulting JSON to a Post. Of course, there is also a fetchCommentsByPostID(post.ID) helper function, which does the same for comments, by fetching the data from http://jsonplaceholder.typicode.com/posts/${id}/comments and transforming it to []Comment.

Then we go on to create our graphQL schema. We start by defining the queryType, which is the root of our schema:

func createQueryType(postType *graphql.Object) graphql.ObjectConfig {
    return graphql.ObjectConfig{Name: "QueryType", Fields: graphql.Fields{
        "post": &graphql.Field{
            Type: postType,
            Args: graphql.FieldConfigArgument{
                "id": &graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.Int),
                },
            },
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                id := p.Args["id"]
                v, _ := id.(int)
                log.Printf("fetching post with id: %d", v)
                return fetchPostByiD(v)
            },
        },
    }}
}

The root query type has only one field - the post. This field is defined by postType, which we will look at shortly. It takes only one argument called id.

Posts are resolved by taking the id from p.Args and passing it to fetchPostsByID, returning the fetched and transformed Post as well as any error.

Next, we define the postType, which is quite interesting. We basically just map the post fields from the data model to graphQL types, but we also add the comments field. The comment’s Resolve function is only called, if the client explicitly wants to fetch them.

To resolve comments, we access the “parent” of this query by using p.Source, which yields us an instance of *Post - the fetched post. Using the id of this post, we can fetch the comments for it:

func createPostType(commentType *graphql.Object) *graphql.Object {
    return graphql.NewObject(graphql.ObjectConfig{
        Name: "Post",
        Fields: graphql.Fields{
            "userId": &graphql.Field{
                Type: graphql.NewNonNull(graphql.Int),
            },
            "id": &graphql.Field{
                Type: graphql.NewNonNull(graphql.Int),
            },
            "title": &graphql.Field{
                Type: graphql.String,
            },
            "body": &graphql.Field{
                Type: graphql.String,
            },
            "comments": &graphql.Field{
                Type: graphql.NewList(commentType),
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    post, _ := p.Source.(*Post)
                    log.Printf("fetching comments of post with id: %d", post.ID)
                    return fetchCommentsByPostID(post.ID)
                },
            },
        },
    })
}

The only type left to define in the schema is the commentType, which is pretty boring, as it only maps fields of the data model to graphQL types:

func createCommentType() *graphql.Object {
    return graphql.NewObject(graphql.ObjectConfig{
        Name: "Comment",
        Fields: graphql.Fields{
            "postid": &graphql.Field{
                Type: graphql.NewNonNull(graphql.Int),
            },
            "id": &graphql.Field{
                Type: graphql.NewNonNull(graphql.Int),
            },
            "name": &graphql.Field{
                Type: graphql.String,
            },
            "email": &graphql.Field{
                Type: graphql.String,
            },
            "body": &graphql.Field{
                Type: graphql.String,
            },
        },
    })
}

Alright, our schema is defined and the only thing left is to put it all together.

We instantiate a graphQL schema and pass it to graphql-go-handler, which is an http-middleware which helps us to deal with graphQL queries. Then we simply start an http server with the returned handler routed to by /graphql.

This is what it looks like:

func main() {
    schema, err := graphql.NewSchema(graphql.SchemaConfig{
        Query: graphql.NewObject(
            createQueryType(
                createPostType(
                    createCommentType(),
                ),
            ),
        ),
    })
    if err != nil {
        log.Fatalf("failed to create schema, error: %v", err)
    }
    handler := gqlhandler.New(&gqlhandler.Config{
        Schema: &schema,
    })
    http.Handle("/graphql", handler)
    log.Println("Server started at http://localhost:3000/graphql")
    log.Fatal(http.ListenAndServe(":3000", nil))
}

Alright, that’s it!

After starting the server, we can use GraphiQL to query for a post with a certain id, specifying the fields we are interested in:

query {
  post(id: 5) {
    userId
    id
    body
    title
    comments {
      id
      email
      name
    }
  }
}

Resulting in the following response:

{
  "data": {
    "post": {
      "userId": 1,
      "id": 5,
      "title": "...",
      "body": "...",
      "comments": [
        {
          "id": 21,
          "email": "...",
          "name": "..."
        }
      ]
    }
  }
}

If we omit comments from the query, the request to fetch the comments is never made and we simply get the selected post as a response.

The complete example code can be found here.

Conclusion

This example showed how to transform an existing REST API to GraphQL using a thin Go layer. The library I used for this, graphql-go worked nicely, provided solid documentation and good examples to follow. Also, it closely mirrors the graphql-js API, which I was already familiar with, which made the conversion to Go a lot easier.

There are surely more concise and fancier ways to define a schema such as this, but due to the introductory nature and my unfamiliarity with graphQL in Go I went for this solution, which is focused, above anything else, on clarity.

GraphQL seems like it is here to stay and for good reason. I hope to be able to touch on GraphQL subscriptions in a future blog post, as well as some other, more advanced use-cases. :)

Resources


I work as a freelance software engineer and trainer and you can hire me. If you like my work, or my writing and if I sound like someone you'd like to work with, let's talk! :)