Introduction to Golang Graphql
In this tutorial, we will examine some examples of graphql
in Golang. GraphQLÂ is an alternative to REST, created by Facebook (http://graphql.org/). This technology allows a server to implement and publish a schema, and the clients then can ask for the information they need, rather than understanding and making use of various API endpoints.
At its simplest, GraphQL is about asking for specific fields on objects. Let's start by looking at a very simple query and the result we get when we run it:
{
hero {
name
}
}
Result:Â
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
You can see immediately that the query has exactly the same shape as the result. This is essential to GraphQL,
because you always get back what you expect, and the server knows exactly what fields the client is asking for.
A simple example of using graphql package
In this example, we will use graphql package, an implementation of GraphQL in Go. To install the library, run:
go get github.com/graphql-go/graphql
The following is a simple example which defines a schema with a single hello
string-type field and a Resolve method which returns the string "Welcome to GoLinuxcloud!". A GraphQL query is performed against this schema with the resulting output printed in JSON format.
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/graphql-go/graphql"
)
func main() {
// Schema
fields := graphql.Fields{
"hello": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return `Welcome to GoLinuxcloud!`, nil
},
},
}
// Define a GraphQL object type named rootQuery
rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)}
schema, err := graphql.NewSchema(schemaConfig)
if err != nil {
log.Fatalf("failed to create new schema, error: %v", err)
}
// Query
query := `
{
hello
}
`
params := graphql.Params{Schema: schema, RequestString: query}
r := graphql.Do(params)
if len(r.Errors) > 0 {
log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
}
rJSON, _ := json.Marshal(r)
fmt.Printf("%s \n", rJSON)
}
Output:
{"data":{"hello":"Welcome to GoLinuxcloud"}}
Reading with thread's parameter from context
Here's an example of reading the thread's parameter from context. We will send a query within a context, and try to read a parameter from that context.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/graphql-go/graphql"
)
func main() {
extractFieldFromContextFn := func(p graphql.ResolveParams) (interface{}, error) {
return p.Context.Value(p.Args["key"]), nil
}
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"value": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"key": &graphql.ArgumentConfig{Type: graphql.String},
},
Resolve: extractFieldFromContextFn,
},
},
}),
})
if err != nil {
log.Fatalf("failed to create new schema, error: %v", err)
}
query := `{ value(key:"User1") }`
result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
Context: context.WithValue(context.TODO(), "User1", "xyz"),
})
rJSON, _ := json.Marshal(result)
fmt.Printf("%s \n", rJSON)
}
Output:
{"data":{"value":"xyz"}}
In the query, we send a key
. Instead of return a fixed string like example 1, in this example the ResolveParams()
returns the value of key in the context:
query := `{ value(key:"User1") }`
Context: context.WithValue(context.TODO(), "User1", "xyz")
{"data":{"value":"xyz"}}
GraphQL server application with http
Set up helper function, struct and data file
First lets define business logic that will expose two methods ListStudents, GetStudent
and Student
type as below:
var students []*Student
type Student struct {
ID int
Name string
Passed bool
}
func ListStudents() ([]*Student, error) {
return students, nil
}
func GetStudent(id int) (*Student, error) {
for _, s := range students {
if s.ID == id {
return s, nil
}
}
return nil, errors.New("Todo not found")
}
All students are saved in a JSON file called data.json
and are loaded when the application starts:
[
{
"id": 1,
"name": "Dan",
"passed": true
},
{
"id": 2,
"name": "Bob",
"passed": true
},
{
"id": 3,
"name": "Daniel",
"passed": true
},
{
"id": 4,
"name": "Erik",
"passed": true
},
{
"id": 5,
"name": "Ben",
"passed": true
},
{
"id": 6,
"name": "Chuck",
"passed": true
}
]
Load the students data.json
file:
content, err := ioutil.ReadFile("./data.json")
if err != nil {
fmt.Print("Error:", err)
}
err = json.Unmarshal(content, &students)
if err != nil {
fmt.Print("Error:", err)
}
GraphQL Schema
First, we'll create an object schema type to represent our Student
struct. This is accomplished using the NewObject
function from the graphql-go/graphql package.
studentType := graphql.NewObject(graphql.ObjectConfig{
Name: "Student",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.Int,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if student, ok := p.Source.(*Student); ok {
return student.ID, nil
}
return nil, nil
},
},
"name": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if student, ok := p.Source.(*Student); ok {
return student.Name, nil
}
return nil, nil
},
},
"passed": &graphql.Field{
Type: graphql.Boolean,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if student, ok := p.Source.(*Student); ok {
return student.Passed, nil
}
return nil, nil
},
},
},
})
This object will have three fields id, name and passed with data type correspond to our Student
struct's field respectively.
The next step is to define the query schema. For the purposes of this example application, we will expose two queries named students
and student,
the first of which will list all students and the second of which will retrieve a specific student by providing an id as a query argument.
queryType := graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"students": &graphql.Field{
Type: graphql.NewList(studentType),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return ListStudents()
},
},
"student": &graphql.Field{
Type: studentType,
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Int),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return GetStudent(p.Args["id"].(int))
},
},
},
})
As you can see, the actual logic for getting the students list is by calling our server methods defined in the preceding section in the resolve function of each query: ListStudents
and GetStudent
Connect GraphQL with HTTP
Last but not least, the graphql
endpoint should be accessible via http. We could write our own http server implementation to convert request and response back and forth, but there is a package called graphql-go/handler that has already done that for us, so let's use it.
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: false,
Playground: true,
})
The Playground option is only for development purposes, and it allows us to experiment with the query once we navigate to http://localhost:8080/graphql
The complete code:
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/graphql-go/graphql"
"github.com/graphql-go/handler"
)
var students []*Student
type Student struct {
ID int
Name string
Passed bool
}
func ListStudents() ([]*Student, error) {
return students, nil
}
func GetStudent(id int) (*Student, error) {
for _, s := range students {
if s.ID == id {
return s, nil
}
}
return nil, errors.New("Todo not found")
}
func main() {
content, err := ioutil.ReadFile("./data.json")
if err != nil {
fmt.Print("Error:", err)
}
err = json.Unmarshal(content, &students)
if err != nil {
fmt.Print("Error:", err)
}
studentType := graphql.NewObject(graphql.ObjectConfig{
Name: "Student",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.Int,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if student, ok := p.Source.(*Student); ok {
return student.ID, nil
}
return nil, nil
},
},
"name": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if student, ok := p.Source.(*Student); ok {
return student.Name, nil
}
return nil, nil
},
},
"passed": &graphql.Field{
Type: graphql.Boolean,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if student, ok := p.Source.(*Student); ok {
return student.Passed, nil
}
return nil, nil
},
},
},
})
queryType := graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"students": &graphql.Field{
Type: graphql.NewList(studentType),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return ListStudents()
},
},
"student": &graphql.Field{
Type: studentType,
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Int),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return GetStudent(p.Args["id"].(int))
},
},
},
})
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: queryType,
})
if err != nil {
log.Fatal(err)
}
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: false,
Playground: true,
})
http.Handle("/graphql", h)
log.Fatal(http.ListenAndServe(":8080", nil))
}
The final step is to open up the endpoint and experiment with it, as shown in the image below:
Query:
Response:
Summary
In this tutorial, I defined a GraphQL schema and query it in Golang. For more complex examples, refer to the examples graphql_test.go.
References
https://github.com/graphql-go/graphql
https://graphql.org/