An Introduction to GraphQL — Part 2 — Server implementation
Originally posted on Medium - 6th May 2019
In part one, we covered how we got here, so how about some code?
I’ve picked Go for the code (server side) in this, no particular reason beyond that’s what I’ve been playing with lately (and also to show that we don’t just have to use JavaScript). The style of coding GraphQL libraries tends to be fairly similar between the languages, so you should be able to follow along.
package main
import (
"encoding/json"
"github.com/graphql-go/graphql"
"github.com/graphql-go/handler"
"io/ioutil"
"math/rand"
"net/http"
"time"
)
So we start with the libraries we’re going to use. encoding/json because we’ll convert REST endpoints in to GraphQL for this example. github.com/graphql-go/* is the library we’re using here. net/http for our server, and the rest are just for our various dummy responses.
Now GraphQL has built in types and user types (which ultimately are made up of the built in types right at the bottom). So let’s define some of our types that make up our schema.
var userType = graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Fields: graphql.Fields{
"name": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
"accountList": &graphql.Field{
Type: graphql.NewList(accountType),
},
},
})
var accountType = graphql.NewObject(graphql.ObjectConfig{
Name: "accountList",
Fields: graphql.Fields{
"accNo": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
},
"balance": &graphql.Field{
Type: graphql.NewNonNull(graphql.Float),
},
},
})
var addressType = graphql.NewObject(graphql.ObjectConfig{
Name: "address",
Fields: graphql.Fields{
"Num": &graphql.Field{
Type: graphql.String,
},
"Street": &graphql.Field{
Type: graphql.String,
},
"Type": &graphql.Field{
Type: graphql.String,
},
},
})
We’ve defined three types here. A user, which contains a name and a list of accounts. The account type which is used by the user. And an address type.
As you can see, these ultimately end up being a bunch of Strings, Ints and Floats.
Next up, we actually define our Query Schema. This is the same as the other types, but we define the resolvers here also (these can be in the types as well, so this is just an over encompassing type). In reality, we’d separate the resolvers into external functions for readability, but I’ve left them as anonymous functions for ease in this article.
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
var queryType = graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"lastestPost": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
Description: "Hello Desc",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return "Hello World!", nil
},
},
"random": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
Description: "Returns a random number",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return r.Int31(), nil
},
},
"double": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
Description: "Doubles the input number",
Args: graphql.FieldConfigArgument{
"val": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Int),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
val, _ := p.Args["val"].(int)
return val * 2, nil
},
},
"user": &graphql.Field{
Type: userType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
// Read JSON, and spit out GraphQL
response, err := http.Get("http://localhost:4545/test")
if err != nil {
return nil, err
}
var dat map[string]interface{}
data, _ := ioutil.ReadAll(response.Body)
if err := json.Unmarshal(data, &dat); err != nil {
return nil, err
}
return dat, nil
},
},
"address": &graphql.Field{
Type: addressType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
// Read JSON, and spit out GraphQL
response, err := http.Get("http://localhost:4546/address")
if err != nil {
return nil, err
}
var dat map[string]interface{}
data, _ := ioutil.ReadAll(response.Body)
if err := json.Unmarshal(data, &dat); err != nil {
return nil, err
}
return dat["Address"], nil
},
},
},
})
So let’s break this down a bit. The easy bits are the “latest post”, “random” and “double” sections. Each of these has a type (String, Int and Int respectively), a Description (for our automatic documentation) and a Resolve function that does the actual work. By defining our return types as “NewNonNull” we can always guarantee these will be defined. To make these nullable would break the schema in future revisions, so make sure they will never be nullable. Err on the side of caution here and make them nullable (then check in the client side code).
Additionally our “double” section can take arguments. Once again, Non-nullable (ie. required field). The beauty of GraphQL is that if we try to call this without an argument, an error will be returned. Additionally if we try to call this with a String instead of an Int, an error will also be returned. Self documenting code is wonderful.
The “user” and “address” sections are practically the same, and are just a quick and dirty method of taking in JSON from a REST endpoint and passing it along. Specifying fields in your GraphQL request does all the magic of only passing back the required data. In a production system, the reality is that you’ll probably want to do a bit more checking of the response here.
var Schema, _ = graphql.NewSchema(graphql.SchemaConfig{
Query: queryType,
})
func main() {
h := handler.New(&handler.Config{
Schema: &Schema,
Pretty: true,
GraphiQL: false,
Playground: true,
})
http.Handle("/graphql", h)
http.ListenAndServe(":8090", nil)
}
Finally we set our schema query to be this type (later on, this is where we add a mutation type). Then we fire up the server.
For the full project, I’ve put up a basic repo on github. The mocks.sh is for adding mocks to a running Mountebank server.
So now we have a basic server, we can actually play with this without even writing a client. Run the server (typically “go get && go run main.go”) then browse to http://localhost:8090/graphql. You’ll be greeted with the following screen.
So a pretty simple interface. Enter your query on the left, hit play and you’ll get a result on the right. The beauty is you have documentation (tab on the far right) AND autocomplete.
Let’s start with something basic.
{
random
}
Enter that, click play, and you should see the following (with the number likely to be different).
Simple. But as this query is one big type, we can get multiple things at once, try the following
{
random
lastestPost
double(val: 10)
test: double(val: 50)
}
Hold up a minute, where are the commas, and what’s that test thing at the bottom? Well, to answer the first, we’re only JSON-like, not actual JSON. Feel free to add the commas if you like, but they’re entirely optional.
Now to the second part, the “test” at the bottom is aliasing a field. The beauty of this is it means you can request the same data multiple times just by giving it a different alias (think of a SQL query where you join a table to itself, you have to alias at least one of those tables to refer to it correctly).
Feel free to play around with this a little. Try entering a string into the “val” of double. Request a field that doesn’t exist, etc.
Now to get a bit more complex. Let’s query in to one of those more complex types.
{
user {
name
}
address {
Num
Street
Type
}
}
Not much harder is it? You just have to specify the fields you’re after within the types. Here, the user type also has additional data, but we’re not requesting it, so it doesn’t come back. This is HUGE. You can save quite a lot of bandwidth and unnecessary processing here. Those of us with data caps will love you for it.
One of the last things I’ll be showing you in this intro is naming queries.
query Blah {
user {
name
}
address {
Num
Street
Type
}
}
In your client side code, your queries are more likely to look like this, especially when using a library like Apollo that does some code generation for you. Otherwise it has no clue what to call your objects.
So querying data is all well and good, how do we go about changing data? That’s where mutations come in. Much like with REST, there’s nothing to physically stop you doing it in your normal queries (or GET with REST), but like all good programmers, you like you code to be correct don’t you?
So let's extend our little Go server (or check out the mutate branch if using the code from my github).
We need to add variable to store our mutating data to. Yes, not good practice having a global variable, but this is just example code. Add the latestPost line(extra code around for context).
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
var lastestPost atomic.Value // Add this line in to store our data that mutates
// Start Return Types for User
var userType = graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Fields: graphql.Fields{
"name": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
"accountList": &graphql.Field{
Type: graphql.NewList(accountType),
},
},
})
Find where we are returning the latestPost in the query and update it to return the new variable instead.
"lastestPost": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
Description: "Hello Desc",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
ret := lastestPost.Load().(string)
return ret, nil
},
},
Update the main function to store a default value for latestPost.
func main() {
lastestPost.Store("Hello World!")
h := handler.New(&handler.Config{
Schema: &Schema,
Pretty: true,
GraphiQL: false,
Playground: true,
})
http.Handle("/graphql", h)
http.ListenAndServe(":8090", nil)
}
Finally we’ll create our mutation and add it to our schema
var mutationType = graphql.NewObject(graphql.ObjectConfig{
Name: "Mutation",
Fields: graphql.Fields{
"lastestPost": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
Description: "Hello Desc",
Args: graphql.FieldConfigArgument{
"newPost": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
lastestPost.Store(p.Args["newPost"])
ret := lastestPost.Load().(string)
return ret, nil
},
},
},
})
var Schema, _ = graphql.NewSchema(graphql.SchemaConfig{
Query: queryType,
Mutation: mutationType,
})
As you can see, it’s pretty simple. We take an argument for the new value, and just set it, then we return that latestPost value as we normally would in a query (this is standard practice to make updates easier on the client side).
So to actually change this value, fire up your browser and point it to your server to bring up the playground and enter the following.
mutation {
lastestPost(newPost: "Test")
}
All the same rules from queries still apply like naming your requests, but that’s all there is to it.
In the final part of this series, we’ll explore making a basic client to read this using Apollo in JavaScript.