Introduction

As you can see in AWS AppSync and Prisma, GraphQL is not only for frontend apps but also for making a data layer encapsulating various DBs. Because Go has nice type-safety and concurrency model, it is reasonable to implement a GraphQL server in Go.

Choosing a GraphQL module for Go

There are some GraphQL modules for Go. They differs in design such as schema-handling. Because graph-gophers/graphql-go seems close to Apollo, I use it in this article.

In this article, I call graph-gophers/graphql-go as graphql-go.

Schema

Similar to Apollo, graphql-go uses GraphQL schema as its input. In schema, you can define types, queries, mutations etc.

const Schema = `

type Vegetable {

name: String!

price: Int!

image: String

} type Query {

vegetable(name: String!): Vegetable

} schema {

query: Query

}

`

It can be parsed and served as following.

import (

"context"

"log"

"net/http"

"strings" "github.com/friendsofgo/graphiql"

graphql "github.com/graph-gophers/graphql-go"

"github.com/graph-gophers/graphql-go/relay"

) // TODO: Schema // TODO: Model type query struct{} // TODO: Resolver func main() {

schema := graphql.MustParseSchema(s, &query{})

http.Handle("/query", &relay.Handler{Schema: schema}) // TODO: init model // TODO: graphiql // Run

log.Println("Server ready at 8080")

log.Fatal(http.ListenAndServe(":8080", nil))

}

At this point, you will encounter a panic error which says there’s no method named vegetable .

GraphQL IDE for Go

Having a visual IDE is really helpful when you create queries browsing actual data. There’s a good module for Go, named graphiql .

Add these lines replacing graphiql comment.

// First argument must be same as graphql handler path

graphiqlHandler, err := graphiql.NewGraphiqlHandler("/query")

if err != nil {

panic(err)

} http.Handle("/", graphiqlHandler)

Implementing resolvers

Let’s proceed to implementing resolvers. Resolvers are structs which have fields defined in the schema file.

It is possible to use model structs as resolvers but I don’t recommend it because resolvers and models may have different manner in null handling. In graphql-go, nullable types are pointer types.

type VegetableResolver struct {

v *Vegetable

} func (r *VegetableResolver) Name() string { return r.v.name }

func (r *VegetableResolver) Price() int32 { return int32(r.v.price) }

func (r *VegetableResolver) Image() *string { return r.v.image }

The resolver above is returned by a query resolver which accepts args.

It must be a method of query object.

Query parameters are passed as 2nd argument in struct.

func (q *query) Vegetable(ctx context.Context, args struct{ Name string }) *VegetableResolver {

v, ok := vegetables[strings.ToLower(args.Name)]

if ok {

return &VegetableResolver{v: &v}

}

return nil

}

You should use Context created per request instead of Background. It matters when you use Stackdriver.

Thus, resolvers are associated with schema.

Data model

To simplify this example, let’s consider a simple map of struct as our data source. You can add tags to fields which your ORM requires.

type Vegetable struct {

name string

price int

image *string

} var vegetables map[string]Vegetable // Utils

func strPtr(str string) *string {

return &str

}

Initialization in func main is:

// init model

vegetables = map[string]Vegetable{

"tomato": Vegetable{name: "Tomato", price: 100, image: strPtr("https://picsum.photos/id/152/100/100")},

"potato": Vegetable{name: "Potato", price: 50, image: strPtr("https://picsum.photos/id/159/100/100")},

"corn": Vegetable{name: "Corn", price: 200},

}

By running a main.go containing all code fragments and opening http://localhsot:8080/ , you can try queries like this.

Conclusion