Comparison
Currently, the main implementation of GraphQL in Go is https://github.com/graphql-go/graphql. However, creating GraphQL APIs using it can get quite tedious and messy.
Let's look at an example to create a simple GraphQL Schema
enum UserType { ADMIN USER}
type User { id: String! name: String! type: UserType! oldType: UserType! @deprecated(reason: "Old Field")}
type Post { id: String! body: String author: User! # When the post was posted timestamp: Int!}
type Query { user(id: String!) User post(id: String!) Post}What the code looks like with github.com/graphql-go/graphql
func main() { userTypeEnum := graphql.NewEnum(graphql.EnumConfig{ Name: "UserType", Values: graphql.EnumValueConfigMap{ "ADMIN": &graphql.EnumValueConfig{ Value: "ADMIN", }, "USER": &graphql.EnumValueConfig{ Value: "USER", }, }, })
userObject := graphql.NewObject(graphql.ObjectConfig{ Name: "User", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Resolve: func(p graphql.ResolveParams) (interface{}, error) { return "anid", nil }, }, "name": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Resolve: func(p graphql.ResolveParams) (interface{}, error) { return "A Name", nil }, }, "type": &graphql.Field{ Type: graphql.NewNonNull(UserTypeEnum), Resolve: func(p graphql.ResolveParams) (interface{}, error) { return "ADMIN", nil }, }, "oldType": &graphql.Field{ Type: graphql.NewNonNull(UserTypeEnum), DeprecationReason: "Old Field", Resolve: func(p graphql.ResolveParams) (interface{}, error) { return "ADMIN", nil } }, }, })
postObject := graphql.NewObject(graphql.ObjectConfig{ Name: "Post", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Resolve: func(p graphql.ResolveParams) (interface{}, error) { return "anid", nil }, }, "body": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { return "a body", nil }, }, "timestamp": &graphql.Field{ Type: graphql.NewNonNull(graphql.Int), Resolve: func(p graphql.ResolveParams) (interface{}, error) { return 12345, nil }, Description: "When the post was posted", }, "author": &graphql.Field{ Type: graphql.NewNonNull(userObject), Resolve: func(p graphql.ResolveParams) (interface{}, error) { return nil, nil }, }, }, })
queryObject := graphql.NewObject(graphql.ObjectConfig{ Name: "Query", Fields: graphql.Fields{ "user": &graphql.Field{ Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Type: userObject, Resolve: func(p graphql.ResolveParams) (interface{}, error) { return db.User.Get(p.Args["id"]) }, }, "post": &graphql.Field{ Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Type: postObject, Resolve: func(p graphql.ResolveParams) (interface{}, error) { return db.Post.Get(p.Args["id"]) }, }, }, })
schema := graphql.NewSchema(graphql.SchemaConfig{ Query: queryObject, })}As you can see, there are quite a few drawbacks to doing this. First, interface{}'s are used everywhere we're not taking advantage of the type system in Go. Second, the code might become unnecessarily lengthy.
In contrast, here's how you'd do it with Groot
type UserType string
const ( UserTypeAdmin UserType = "ADMIN" UserTypeUser UserType = "USER")
func (u UserType) Values() []string { return []string{string(UserTypeAdmin), string(UserTypeUser)}}
type User struct { ID groot.ID `json:"id"` Name string `json:"name"` Type UserType `json:"type"` OldType UserType `json:"oldType" deprecate:"Old Field"`}
type Post struct { ID groot.ID `json:"id"` // you can use a pointer to mark the field as nullable Body *string `json:"body"` Timestamp int `json:"timestamp" description:"When the post was posted"` Author User `json:"author"`}
type Query struct { User *User `json:"user"` Post *Post `json:"post"`}
type IdArgs struct { ID groot.ID `json:"id"`}
func (query Query) ResolveUser(args IdArgs) (User, error) { return db.User.Get(args.ID)}
func (query Query) ResolvePost(args IdArgs) (Post, error) { return db.Post.Get(args.ID)}
func main() { schema := groot.NewSchema(groot.SchemaConfig{ Query: groot.MustParseObject(Query{}) })}As you can see, the code is much more readable, and takes full advantage of the Go type system. Groot also comes with default resolvers, and type checks the resolvers on startup. It's also intercompatible with gtihub.com/graphql-go/graphql since it uses github.com/graphql-go/graphql under the hood, and just provides a really nice abstraction on top of it. This means almost all extensions and libraries meant to be used with github.com/graphql-go/graphql can continue to be used with Groot.
No generated code, no boilerplate, composable, and type safe!