Construct and structure a Go GraphQL API

Construct and structure a Go GraphQL API


GraphQL was released 2 years ago and applications that use it are still rare. However, this implementation proposed by Facebook offers many possibilities that are not available in REST APIs.

Introduction

The goal of this blog post is not to explain from the basics what is GraphQL because official documentation of the implementation located at http://graphql.org/learn explain it really well!

I was interested of constructing a GraphQL API and I also wanted to have a performant API so I choose the Go language to develop it using the graphql-go library available here: https://github.com/graphql-go/graphql.

File structure of our API

The first thing to do (and not least!) is to create a great understandable and re-usable file structure.

Indeed, our API will have to evolve in the future, for sure, so we will have to add more and more files into it such as maybe a security component, a rate limiter component, something to log requests, and so many more.

Here is the file structure I propose to create in this blog post that seems to me to be a great start:

. ├── app │ ├── config.go │ ├── config.json │ └── config_test.go ├── security │ ├── security.go │ └── security_test.go ├── mutations │ ├── mutations.go │ ├── mutations_test.go │ ├── user.go │ └── user_test.go ├── queries │ ├── queries.go │ ├── queries_test.go │ ├── user.go │ └── user_test.go ├── types │ ├── role.go │ ├── role_test.go │ ├── user.go │ └── user_test.go └── main.go

Here is what we have here:

  • "app/" will contains all Go classes and configuration files that will be needed to get our application (API) to work so we will have mainly a configuration file (JSON) config.json and also a config.go file that will load this JSON,
  • "security/" will group Go classes that will be used for our security component,
  • "mutations/" will group all of our GraphQL mutations (data changes),
  • "queries/" will group all of our GraphQL queries (selecting data),
  • "types/" will group all of our Go structures and types used by GraphQL by both queries and mutations.

Finally, we will find at the root directory the main.go file which will be the entry point of our API.

So now that everything is clear, let's start writing our API!

API entry point

To construct our API, we will need in a first time the Go net/http package (because our GraphQL API will be accessible over HTTP) and also the graphql-go library:

package main import ( "log" "net/http" "github.com/graphql-go/graphql" "github.com/graphql-go/handler" ) func main() { // Todo: Implement GraphQL handler http.Handle("/", httpHandler) log.Print("ready: listening...\n") http.ListenAndServe(":8383", nil) }

You can note here that the httpHandler variable that will be the HTTP handler used by GraphQL and will be executed for each request that will be made over the "/" URL. Also, we have to give a port number here (8383) on which our API will listen to.

Let's implement the httpHandler which will need a schema in which we will specify two entry points: a first for the queries and a second for the mutations:

schemaConfig := graphql.SchemaConfig{ Query: graphql.NewObject(graphql.ObjectConfig{ Name: "RootQuery", Fields: queries.GetRootFields(), }), Mutation: graphql.NewObject(graphql.ObjectConfig{ Name: "RootMutation", Fields: mutations.GetRootFields(), }), } schema, err := graphql.NewSchema(schemaConfig) if err != nil { log.Fatalf("Failed to create new schema, error: %v", err) } httpHandler := handler.New(&handler.Config{ Schema: &schema })

In the case you have no data modifications but only queries, you can of course remove the mutations section from the code.

Here, we are missing the queries.GetRootFields() and also the mutations.GetRootFields() methods. These methods will allow us to define our queries and mutations that will be used in our API.

Rather than weighing down our main.go file with these things, I chose to put them into two separate files: queries/queries.go and mutations/mutations.go.

Data structures

Before to go writting our first query, we will need to define our data model.

In this blog post, we will return user data with an identifier, a firstname and a lastname. So we will have to write our first file under types/user.go:

package types import ( "github.com/graphql-go/graphql" ) // User type definition. type User struct { ID int `db:"id" json:"id"` Firstname string `db:"firstname" json:"firstname"` Lastname string `db:"lastname" json:"lastname"` } // UserType is the GraphQL schema for the user type. var UserType = graphql.NewObject(graphql.ObjectConfig{ Name: "User", Fields: graphql.Fields{ "id": &graphql.Field{Type: graphql.Int}, "firstname": &graphql.Field{Type: graphql.String}, "lastname": &graphql.Field{Type: graphql.String}, }, })

We declare two things here:

  • A Go struct that will be used for mapping data from our database and to return data to the client in a JSON format,
  • A UserType GraphQL Field that will be used by our GraphQL API to specify fields that can be returned by our API.

By using the data model, we are now (finally) able to write our first query!

Queries

Let's edit the queries/queries.go file in order to add a user named query that will be charged to return our user data:

package queries import ( "github.com/graphql-go/graphql" ) // GetRootFields returns all the available queries. func GetRootFields() graphql.Fields { return graphql.Fields{ "user": GetUserQuery(), } }

We've added a new field in our root query previously written named user and that will call the GetUserQuery() function to return its fields. This function will be defined into another file.

We will now define this function and its behavior into another dedicated file called queries/user.go:

package queries import ( "../types" "github.com/graphql-go/graphql" ) // GetUserQuery returns the queries available against user type. func GetUserQuery() *graphql.Field { return &graphql.Field{ Type: graphql.NewList(types.UserType), Resolve: func(params graphql.ResolveParams) (interface{}, error) { var users []types.User // ... Implement the way you want to obtain your data here. return users, nil }, } }

Our first query is ready: we will use the data model UserType for it so you just have to implement the logic to retrieve your data here.

At this place, you can make a call on every storage engine you want to obtain your data: relational databases or not, SQL or not, file storage, memory storage, everything is possible.

Add relations to your API

Let's now imagine that you have some roles defined for your users (in order to manage access to some resources).

You can also ask to your GraphQL API to return them.

To do that, we will implement a new Role struct and also a RoleType used by GraphQL.

Create the types/role.go containing following code:

package types import ( "github.com/graphql-go/graphql" ) // Role type definition. type Role struct { ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` } // RoleType is the GraphQL schema for the user type. var RoleType = graphql.NewObject(graphql.ObjectConfig{ Name: "Role", Fields: graphql.Fields{ "id": &graphql.Field{Type: graphql.Int}, "name": &graphql.Field{Type: graphql.String}, }, })

We now have to specify to our UserType and we can also obtain some roles linked to the user.

To do that, edit the types/user.go file and add a new graphql.Field section to RoleType:

var UserType = graphql.NewObject(graphql.ObjectConfig{ Name: "User", Fields: graphql.Fields{ // ... already defined fields "roles": &graphql.Field{ Type: graphql.NewList(RoleType), Resolve: func(params graphql.ResolveParams) (interface{}, error) { var roles []Role // userID := params.Source.(User).ID // Implement logic to retrieve user associated roles from user id here. return roles, nil }, }, }, })

Please note that the Type specified for this field is a graphql.NewList(RoleType) because we will return a roles list and not a single role entry.

To request against user roles using the current user data, you can use the available params.Source.

Finally, what is interesting here is that roles queries will be only done if the roles data are requested by the GraphQL API client.

Make calls to your API

Starting from there, you are able to call your API with a query like that:

curl -X POST -H 'Content-Type: application/json' -d '{"query": "query { users { id,firstname,lastname,roles{name} } }"}' http://localhost:8383/ {"data":{"user":[{"id":1,"firstname":"Vincent","lastname":"COMPOSIEUX","roles":[]}, ...]}}

Of course, as said previously, only query specified fields will be returned, that's the GraphQL main principle.

GraphQL also offers a lot of great things to help you writing neat queries with among others aliases, variables and fragments.

You can read documentation of these parts here:

Mutations

About mutations, le fonctionnement est identique aux requêtes. Nous allons donc créer notre première mutation et vous allez voir que ça ressemble beaucoup aux queries.

Create file mutations/mutations.go and specify the RootMutation returned by GetRootFields():

package mutations import ( "github.com/graphql-go/graphql" ) // GetRootFields returns all the available mutations. func GetRootFields() graphql.Fields { return graphql.Fields{ "createUser": GetCreateUserMutation(), } }

Then, we will create a mutation to create a new user in our database.

Let's declare the GetCreateUserMutation() function in file mutations/user.go:

package mutations import ( "../types" "github.com/graphql-go/graphql" ) // GetCreateUserMutation creates a new user and returns it. func GetCreateUserMutation() *graphql.Field { return &graphql.Field{ Type: types.UserType, Args: graphql.FieldConfigArgument{ "firstname": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, "lastname": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { user := &types.User{ Firstname: params.Args["firstname"].(string), Lastname: params.Args["lastname"].(string), } // Add your user in database here return user, nil }, } }

Your mutation is now ready to be used!

As you can notice, we've added an Args section here that allows us to define some arguments to our function, such as: createUser(firstname: "John", lastname: "Snow").

This is of course possible to test our API by calling it now using the following way:

curl -X POST -H 'Content-Type: application/json' -d '{"query": "mutation { createUser(firstname: \"John\", lastname: \"Snow\") { id,firstname,lastname } }"}' http://localhost:8383

You can of course choose only the identified of the newly created user if needed.

Security

Most of your APIs are certainly not public so you also need a component to handle security and that's whay we will do by using a JWT authentication (https://jwt.io/).

We will use the dgrijalva/jwt-go library (https://github.com/dgrijalva/jwt-go) in order to simplify the use of JWT in our Go application.

Simply add in your security/security.go file the following content:

package security import ( "fmt" "log" "net/http" jwt "github.com/dgrijalva/jwt-go" ) // Handle security middleware aims to implement a JWT authentication. func Handle(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tokenString := r.Header.Get("Authorization")[7:] // 7 corresponds to "Bearer " token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } var secret = "my-high-security-secret" // Prefer to store this secret in a configuration file return []byte(secret), nil }) if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { log.Printf("JWT Authenticated OK (app: %s)", claims["app"]) next.ServeHTTP(w, r) } }) }

We retrieve the token sent in the Authorization: Bearer xxx header by the client and will compare it with a secret we've stored in our configuration file.

In the case the token is valid, the application will continue to execute the HTTP handler, elsewhere, an error will be thrown.

In order to use this security component, we will have to update our main.go file to import the security folder and also modify:

http.Handle("/", httpHandler)

with:

http.Handle("/", security.Handle(httpHandler))

You now have a functional and secured GraphQL API!

Conclusion

The GraphQL implementation using Go is quite simple to do thank to the available library and language performances allows to construct powerful APIs.

It is also possible to well structure your API in order to separate, especially for queries, mutations and other components.

If you want to give it a try, sources of this blog post are available here: https://github.com/eko/graphql-go-structure

Author(s)

Vincent Composieux

Vincent Composieux

Architecte passionné par les technologies web depuis de longues années, je pratique principalement du PHP (Symfony) / Javascript mais aussi du Python ou Golang.

View profile

You wanna know more about something in particular?
Let's plan a meeting!

Our experts answer all your questions.

Contact us

Discover other content about the same topic

Deploy a replicated MongoDB on AWS with Terraform and Ansible

Deploy a replicated MongoDB on AWS with Terraform and Ansible

I recently had the opportunity to deploy a MongoDB server on Amazon Web Services (AWS). In order to limit the problems of crash and data loss, it is also replicated with two other servers, ideally in a different geographical area to ensure high availability.