An easier way to create GraphQL APIs in Go. View the full documentation here.
Groot is built on top of github.com/graphql-go/graphql
, which means it should support most existing tooling built for it.
Go already has a couple of implementation of GraphQL, so why another one?
Go is statically typed, and GraphQL is type safe, which means we don't need to and shouldn't use interface{}
anywhere. A simple user struct with custom resolvers would look something like this. Although most type checking is done by Go, additional checks like resolver return types are done by Groot on startup to avoid type errors altogether.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
Although the schema first aproach has its advantages, code first is arguably easier to maintain in the long run without having to deal with federated schemas and schema stitching. For more info check out this blog post.
When you work with Groot, in a way you're defining your schema first as well since you're defining the structure of your data (struct, interfaces, enums, etc) first.
The only thing we want to worry about is our types, resolvers, and business logic, nothing more. We also don't want to redeclare our types in Go as well as GraphQL, it can get cumbersome to maintain and keep track of.
Seriously, it is.
Let's see how we can create the below GraphQL schema.
type User {
id: ID!
name: String!
email: String!
posts: [Post!]
}
type Post {
id: ID!
title: String!
body: String!
author: User!
timestamp: Int!
}
type Query {
user(id: ID!): User
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!, password: String!): User
createPost(title: String!, body: String!): Post
}
We can define User
and Post
objects as regular structs.
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
// we use a pointer to make the field nullable
Posts *[]Post `json:"posts"`
}
type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
Author User `json:"author"`
Timestamp int64 `json:"timestamp"`
}
Yes, that's it!
The library will automatically generate the resolvers for all the fields. But, you can create custom resolvers for any field by defining a method with the name Resolve{field-name}
. For example, if we want to define a custom resolver for the Posts
field on User
we can write the below method:
func (user User) ResolvePosts() (*[]Post, error) {
posts, err := db.GetPostsByUserID(user.ID)
if err != nil {
return nil, err
}
return posts, nil
}
If the return type of the resolver is not the same as the return type of the field, Groot will panic on startup. For more details on resolvers and arguments check the Field Resolvers section.
The Query
type is just another type with a special name, which means we can define it just like we defined the User
and Post
types with custom resolvers.
type Query struct {
// we use pointers to make the field nullable
User *User `json:"user"`
Post *Post `json:"post"`
}
type IDArgs struct {
ID string `json:"id"`
}
func (q Query) ResolveUser(args IDArgs) (*User, error) {
user, err := db.GetUserByID(args.ID)
if err != nil {
return nil, err
}
return user, nil
}
func (p Query) ResolvePost(args IDArgs) (*Post, error) {
post, err := db.GetPostByID(args.ID)
if err != nil {
return nil, err
}
return post, nil
}
Notice how we were able to accept arguments by having the type of first argument of the resolver as a struct.
For a larger schema, you may think we would need to define a lot of fields and methods on the single Query
type. While you would be right, we can avoid that by just embedding structs. You can find more info on embedding and composition in the Composition section.
Similar to the Query
struct we can define a Mutation
struct to define our mutations.
type Mutation struct {
CreateUser *User `json:"createUser"`
CreatePost *Post `json:"createPost"`
}
type NewUserArgs struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
type NewPostArgs struct {
Title string `json:"title"`
Body string `json:"body"`
}
func (m Mutation) ResolveCreateUser(args NewUserArgs) (*User, error) {
user, err := db.CreateUser(args.Name, args.Email, args.Password)
if err != nil {
return nil, err
}
return user, nil
}
func (m Mutation) ResolveCreatePost(args NewPostArgs) (*Post, error) {
post, err := db.CreatePost(args.Title, args.Body)
if err != nil {
return nil, err
}
return post, nil
}
Finally, to create the schema, we can use the NewSchema
function. We can also use the github.com/graphql-go/handler
library to create a handler for the schema since groot.NewSchema
returns a schema of type graphql.Schema
where the graphql
package refers to the github.com/graphql-go/graphql
library.
import (
"reflect"
"net/http"
"github.com/shreyas44/groot"
"github.com/graphql-go/handler"
)
func main() {
schema := groot.NewSchema(groot.SchemaConfig{
Query: groot.MustParseObject(Query{}),
Mutation: groot.MustParseObject(Mutation{}),
})
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
Playground: true,
})
http.Handle("/graphql", h)
log.Fatal(http.ListenAndServe(":8080", nil)
}
Note, the library isn't tested yet.