Back

Intro GraphQL in Go

What is GraphQL, why to use it, and how to use it in Golang

by Percy Bolmér, September 14, 2021

GraphQL as an architectural solution in Go — Gopher is drawn by [Takuya Ueda](https://twitter.com/tenntenn), inspired by the works of Renée French (CC BY 3.0)
GraphQL as an architectural solution in Go — Gopher is drawn by [Takuya Ueda](https://twitter.com/tenntenn), inspired by the works of Renée French (CC BY 3.0)

In this article we will be building a hiring agency API for gophers, using a GraphQL server built in Go. Unlike many other introductions out there I won’t be listing the name of all components with a short declaration. Instead, we will build an application step by step and explain what is going on in each step. Hopefully, this approach makes it a bit easier to not flood you with information. I wont only cover GraphQL, but also how I structure the architecture when using graphql-go

The first thing we need to clear out is a very common misconception.

GraphQL is not a database! — GraphQL

I think this misunderstanding comes from the name, developers are used to hearing all the database names MySQL, PostgreSQL, etc. What is important is the last two letters, QL. If you’ve been around for some time, you might know this is short for Query Language but many I’ve met just relates it to databases.

So, GraphQL is a Query language built to make communicating between servers and clients easier and more reliant. One other thing we need to sort out right away is, GraphQL is database agnostic. This means that it does not care what underlying database is being used.

All images in this article are drawn by Percy Bolmér, the Gopher is drawn by Takuya Ueda, inspired by the works of Renée French. The gopher has been modified in the images.

How is that possible you ask? Well, you should consider GraphQL as a middleware between your database and client. You have a GraphQL server that receives requests in the Query format, the query specifies exactly what data you want to be returned, the server returns the data defined in the request.

The query language is defined and documented here.

query {
  gopher {
    id 
    name 
  }
  jobs { 
    title 
    years 
  }
}
This is how a query could look when searching for gophers
{
  "data": {
    "gophers": [
      {
        "name": "original",
        "profession": "logotype",
        "jobs": {
          "title": "Original Gopher By Renee Fench",
          "years": 12,
        }
      }
    ]
  }
}
This is how the response would look for the example query, notice that only the wanted fields are present

If you have any further questions about GraphQL, they have a very nice FAQ.

In this article we will cover the basics, we will be building an application for searching and finding gophers to hire. We won’t cover all aspects of GraphQL, that would be too long.

Why and when you should use GraphQL

GraphQL — Using multiple data sources but one response and one request
GraphQL — Using multiple data sources but one response and one request

When I first heard about GraphQL, I admit, I was skeptical. And I’ve understood many others are as well. It sounds like just adding another layer between the API and the caller will make things more complex.

But this is not the case, following along and I hope to convince you of that.

One of the best reasons I find for using GraphQL is the ability to combine data sources. I am a big user of the RepositoryPattern which you can find discussed in my Domain-Driven Design article.

The easy way of learning how to use DDD in a Go application
How To Implement Domain-Driven Design (DDD) in Golang

October 1, 2021

The easy way of learning how to use DDD in a Go application

Read more

In short, you have a repository for each data source. For the agency we are building we will have GopherRepository which stores gophers, and a JobRepository that stores jobs. GraphQL makes it possible to easily combine these two structures into a single output, without coupling the data sources in the backend. In the server we will build, it will look like the Gopher struct has Jobs related to it, but it will actually be two different storage solutions.

The second reason I like GraphQL is the ability to avoid overfetching by allowing the user to specify exactly what fields to request. You only send one request and get only the information you request, no extra fields that are unused.

One of the principles in GraphQL is that the development process should start with the Schema definition. This is called Schema Driven Development and I won’t cover it here, but basically, we start with the schemas instead of the business logic.

In GraphQL your API starts with a schema that defines all your types, queries and mutations, It helps others to understand your API. So it’s like a contract between server and the client. — GraphQL Website

How to use GraphQL in Go

Downloading the required packages and creating a go module
Downloading the required packages and creating a go module

The first thing to do is to decide on what Go package to use. The GraphQL website maintains a list of all available packages.

In this article, we will be using graphql-go/graphql which is a package that is built after the official graphql-js reference. This package does not read graphql schema files, rather we define the schemas in the go code. And the way to define the schemas matches the same way used in the javascript side, so that is nice.

We will begin by creating a package and getting the required libraries to get started. We will be fetching graphql-go/graphql which is used for building and defining our schemas, and graphql-go/handler which is used to host the graphql server.

go mod init github.com/programmingpercy/gopheragency
go get github.com/graphql-go/graphql
go get github.com/graphql-go/handler
touch main.go

Queries and Resolvers — Fetching data

Queries are the requests for data, Resolvers handles the business logic to find the data
Queries are the requests for data, Resolvers handles the business logic to find the data

The query is the operation to fetch data from the server. A query is performed by sending a request according to the Schema declaration. You saw an example query earlier in the article, and we will use that as a starting point. Only the fields used in a query will be returned in the response.

There are two keywords we need to learn before moving on.

  • Field — A value of a certain data type, String, Int, Float, Boolean, and ID
  • Object — An object with fields, think of it like a struct
query {
  gopher { // Object
    id // Field
    name // Field
  }
  jobs { // Object
    title // field
    years // Field
  }
  
}
Example where we show what each thing in a Query is

To begin implementing this we need to start building the schema, remember Schema Driven Development, right?

We will begin by creating the Gopher object that we can query. Create a schema folder and a gopher.go inside of it.

We will begin by creating the GopherType which is a declaration of an object that can be sent using GraphQL. To create a new object in graphql-go/graphql we use the graphql.NewObject function, the input to NewObject is an ObjectConfig.

This ObjectConfig is a struct that is used to configure how the object is defined. The config holds a graphql.Fields object which is an alias for map[string]*graphql.Fields, remember that Objects are a set of Fields.

package schemas

import "github.com/graphql-go/graphql"

// GopherType is the gopher graphQL Object that we will send on queries
// Here we define the structure of the gopher
// This has to match the STRUCT tags that are sent out later
var GopherType = graphql.NewObject(graphql.ObjectConfig{
	Name: "Gopher",
	// Fields is the field values to declare the structure of the object
	Fields: graphql.Fields{
		"id": &graphql.Field{
			Type:        graphql.ID,
			Description: "The ID that is used to identify unique gophers",
		},
		"name": &graphql.Field{
			Type:        graphql.String,
			Description: "The name of the gopher",
		},
		"hired": &graphql.Field{
			Type:        graphql.Boolean,
			Description: "True if the Gopher is employeed",
		},
		"profession": &graphql.Field{
			Type:        graphql.String,
			Description: "The gophers last/current profession",
		},
	},
})
schemas/gopher.go — A declaration of the Gopher Object type

Now that we have defined the GopherType we have to set up and host a GraphQL server that can respond to queries.

To have a GraphQL server we need a RootQuery which is the base for each query. The root query will hold all available queries at the top level.

When a request reaches the server, data has to be fetched. Fetching data is done by Resolvers which is a function that accepts the query and all the arguments. We will create a simple server that responds with a simple Hello first before adding the GopherType.

// This package is a demonstration how to build and use a GraphQL server in Go
package main

import (
	"log"
	"net/http"

	"github.com/graphql-go/graphql"
	"github.com/graphql-go/handler"
)

func main() {

	// We create yet another Fields map, one which holds all the different queries
	fields := graphql.Fields{
		// We define the Gophers query
		"gophers": &graphql.Field{
			// It a String, FOR NOW
			Type: graphql.String,
			// Resolve is the function used to look up data
			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
				return "hello", nil
			},
			// Description explains the field
			Description: "Query all Gophers",
		},
	}
	// Create the Root Query that is used to start each query
	rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
	// Now combine all Objects into a Schema Configuration
	schemaConfig := graphql.SchemaConfig{
		// Query is the root object query schema
		Query: graphql.NewObject(rootQuery)}
	// Create a new GraphQL Schema
	schema, err := graphql.NewSchema(schemaConfig)
	if err != nil {
		log.Fatalf("failed to create new schema, error: %v", err)
	}

	StartServer(&schema)
}

// StartServer will trigger the server with a Playground
func StartServer(schema *graphql.Schema) {
	// Create a new HTTP handler
	h := handler.New(&handler.Config{
		Schema: schema,
		// Pretty print JSON response
		Pretty: true,
		// Host a GraphiQL Playground to use for testing Queries
		GraphiQL:   true,
		Playground: true,
	})

	http.Handle("/graphql", h)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
main.go — A simple graphQL Server that exposes on port :8080

After you’ve updated the main.go to host the server on port 8080 then visit localhost:8080/graphql and you’ll see a UI where we can test the current implementation.

On the left side you can write your Query, in the middle you see the response from the Resolver, and to the right you see a clickable tree structure that you can use to see all available queries.

GraphiQL — A website to test and experiment with Queries
GraphiQL — A website to test and experiment with Queries

Try it out to see if you get the correct response to make sure everything is working.

Now it’s time to implement an actual resolver, we will create a new folder named gopher which will contain a Resolver used to fetch Gophers. Create the resolver.go file which will define all the Resolver functions we expect from data storage for Gophers.

Remember, any function that looks like funcName(p graphql.ResolveParams) (interface{},error) can be used as a resolver.

package gopher

import "github.com/graphql-go/graphql"

type Resolver interface {
	// ResolveGophers should return a list of all gophers in the repository
	ResolveGophers(p graphql.ResolveParams) (interface{}, error)
	// ResolveGopher is used to respond to single queries for gophers
	ResolveGopher(p graphql.ResolveParams) (interface{}, error)
}
gopher/resolver.go — The resolver interface that defines our Gopher Resolver

Now that we have defined a resolver interface, we need to implement it. Let’s use in memory data storage that contains a few gophers. The Gopher struct must have the JSON tags to match the defined GopherType. If the tags don’t match then the response won’t be returned.

Create a gopher.go and fill in the struct.

package gopher

// Has to conform to the schema declaration
type Gopher struct {
	ID         string `json:"id"`
	Name       string `json:"name"`
	Hired      bool   `json:"hired"`
	Profession string `json:"profession"`
}
gopher/gopher.go — The gopher struct that matches the GopherType Object

Let’s create a Repository that defines the functions needed to act as Gopher storage. Create repository.go and insert the following gist.

package gopher

type Repository interface {
	GetGophers() ([]Gopher, error)
	GetGopher(id string) (Gopher, error)
}
gopher/repository.go — A repository interface that defines a Gopher storage

Next, we implement a simple in-memory data repository. We will create a memory.go and fill in the super simple storage solution that generates two gophers for us.

// InMemoryRepository is a storage for gophers that uses a map to store them
type InMemoryRepository struct {
	// gophers is our super storage for gophers.
	gophers []Gopher
	sync.Mutex
}

// NewMemoryRepository initializes a memory with mock data
func NewMemoryRepository() *InMemoryRepository {
	gophers := []Gopher{
		{
			ID:         "1",
			Name:       "Original Gopher",
			Hired:      true,
			Profession: "Logo",
		}, {
			ID:         "2",
			Name:       "Jan",
			Hired:      true,
			Profession: "The Janitor",
		},
	}

	return &InMemoryRepository{
		gophers: gophers,
	}
}

// GetGophers returns all gophers
func (imr *InMemoryRepository) GetGophers() ([]Gopher, error) {
	return imr.gophers, nil
}

// GetGopher will return a goper by its ID
func (imr *InMemoryRepository) GetGopher(id string) (Gopher, error) {
	for _, gopher := range imr.gophers {
		if gopher.ID == id {
			return gopher, nil
		}
	}
	return Gopher{}, errors.New("no such gopher exists")
}
memory.go — The simple storage solution for in memory gophers

Great, we can now start implementing the Resolvers that are used to handle the queries. Let’s begin simple and implement the response with all Gophers available. We will have a struct that fulfills the Resolver interface. The reason why we have a structure this way will become more clear later, but a Resolver can hold many Repositories to combine data.

// GopherService is the service that holds all repositories
type GopherService struct {
	gophers Repository
}

// NewService is a factory that creates a new GopherService
func NewService(repo Repository) GopherService {
	return GopherService{
		gophers: repo,
	}
}

// ResolveGophers will be used to retrieve all available Gophers
func (gs GopherService) ResolveGophers(p graphql.ResolveParams) (interface{}, error) {
	// Fetch gophers from the Repository
	gophers, err := gs.gophers.GetGophers()
	if err != nil {
		return nil, err
	}
	return gophers, nil
}
resolver.go — Added a ResolveGophers function that can be used as a Resolver in the servers root query

To start using the ResolveGophers from the GopherService we need to create a service in main.go and also make the RootQuery return a List of GopherType instead. Remember GopherType was the custom Object we created earlier, and a List is an Array in GraphQL.

func main() {
	// Create the Gopher Repository
	gopherService := gopher.NewService(gopher.NewMemoryRepository())
	// We create yet another Fields map, one which holds all the different queries
	fields := graphql.Fields{
		// We define the Gophers query
		"gophers": &graphql.Field{
			// It will return a list of GopherTypes, a List is an Slice
			// We defined our Type in the Schemas package earlier
			Type: graphql.NewList(schemas.GopherType),
			// We change the Resolver to use the gopherRepo instead, allowing us to access all Gophers
			Resolve: gopherRepo.ResolveGophers,
			// Description explains the field
			Description: "Query all Gophers",
		},
	}
  
  ........
main.go — GraphQL Root query updated to return a List of Gophers and to Resolve them using our InMemoryRepository

Now restart the program go run main.go and visit localhost:8080/graphql, time to see how GraphQL allows us to avoid over fetching and under fetching.

Remember that in GraphQL, we define what fields to return in the query, so only the fields you query will be returned. See the two images below, in I only fetch the Gophers names, the second image shows how to fetch all data available.

GraphiQL — Query to Fetch only the name for all Gophers
GraphiQL — Query to Fetch only the name for all Gophers
GraphiQL — Query to fetch all values available for Gophers
GraphiQL — Query to fetch all values available for Gophers

Combining Data in Queries without Coupling data sources

Combine objects without coupling them
Combine objects without coupling them

Before we move on, It’s time to show how we can use multiple repositories to fetch data from many sources. This is another great feature with GraphQL.

Each graphql.Field has the Resolve field, so you can input a resolve function to each graphql.Field. Usually, we need access to a storage/repository in the resolver, the easiest way to achieve this is by using the Service to generate the Schema as it has all access needed.

Let’s see when we implement a JobRepository which will be used to handle Jobs. We will store both the JobRepository and the GopherRepository in the GopherService, and create a GenerateSchema function in the schema package that accepts the service as input and creates the schema we can use for the GraphQL. This approach allows us to build resolvers that have access to all data sources so we can combine them.

Begin by creating a job folder and create the job structure that we will use internally. We will also create a Repository for the job.


package job

// Repository is used to specify whats needed to fulfill the job storage requirements
type Repository interface {
	// GetJobs will search for all jobs related to and EmployeeID
	GetJobs(employeeID string) ([]Job, error)
}

// Job is how a job is presented
type Job struct {
	ID string `json:"id"`
	// EmployeeID is the employee related to the job
	EmployeeID string `json:"employeeID"`
	Company    string `json:"company"`
	Title      string `json:"title"`
	// Start is when the job started
	Start string `json:"start"`
	// End is when the employment ended
	End string `json:"end"`
}
job/job.go — The job struct used in the backend domain

Next, we need a structure that is part of the repository, this time also an in-memory solution.

// memory is a in memory data storage solution for Job
package job

import (
	"errors"
	"sync"
)

// InMemoryRepository is a storage for jobs that uses a map to store them
type InMemoryRepository struct {
	// jobs is used to store jobs
	jobs map[string][]Job
	sync.Mutex
}

// NewMemoryRepository initializes a memory with mock data
func NewMemoryRepository() *InMemoryRepository {
	jobs := make(map[string][]Job)

	jobs["1"] = []Job{
		{
			ID:         "123-123",
			EmployeeID: "1",
			Company:    "Google",
			Title:      "Logo",
			Start:      "2021-01-01",
			End:        "",
		},
	}
	jobs["2"] = []Job{
		{
			ID:         "124-124",
			EmployeeID: "2",
			Company:    "Google",
			Title:      "Janitor",
			Start:      "2021-05-03",
			End:        "",
		}, {
			ID:         "125-125",
			EmployeeID: "2",
			Company:    "Microsoft",
			Title:      "Janitor",
			Start:      "1980-03-04",
			End:        "2021-05-02",
		},
	}
	return &InMemoryRepository{
		jobs: jobs,
	}
}

// GetJobs returns all jobs for a certain Employee
func (imr *InMemoryRepository) GetJobs(employeeID string) ([]Job, error) {

	if jobs, ok := imr.jobs[employeeID]; ok {
		return jobs, nil
	}
	return nil, errors.New("no such employee exist")

}
job/memory.go — A in memory solution for a job Repository

The last thing to do before we start fixing the Resolvers is to upgrade the Service so it has access to a JobRepository

type Resolver interface {
	// ResolveGophers should return a list of all gophers in the repository
	ResolveGophers(p graphql.ResolveParams) (interface{}, error)
	// ResolveGopher is used to respond to single queries for gophers
	ResolveGopher(p graphql.ResolveParams) (interface{}, error)
	// ResolveJobs is used to find Jobs
	ResolveJobs(p graphql.ResolveParams) (interface{}, error)
}

// GopherService is the service that holds all repositories
type GopherService struct {
	gophers Repository
	// Jobs are reachable by the Repository
	jobs job.Repository
}

// NewService is a factory that creates a new GopherService
func NewService(repo Repository, jobrepo job.Repository) GopherService {
	return GopherService{
		gophers: repo,
		jobs:    jobrepo,
	}
}
gopher/resolver.go — The gopher service now accepts a Job repository

Now it’s time to pay focus on what happens here. We will add a ResolveJobs function in this function we will access a Source field, this field is the Parent of the object. This is very useful when we want to use data from the query itself, like in this case when we search for a Job we need the ID of the Gopher.

The Source will be a Gopher object, so we need to typecast it. Then use the ID of that gopher to the jobRepository

// ResolveJobs is used to find all jobs related to a gopher
func (gs *GopherService) ResolveJobs(p graphql.ResolveParams) (interface{}, error) {
	// Fetch Source Value
	g, ok := p.Source.(Gopher)

	if !ok {
		return nil, errors.New("source was not a Gopher")
	}
	// Find Jobs Based on the Gophers ID
	jobs, err := gs.jobs.GetJobs(g.ID)
	if err != nil {
		return nil, err
	}
	return jobs, nil
}
gopher/resolver.go — ResolveJobs will want the parent object to be a gopher

Time to start building the schema, this is where we can combine data from the GopherService.

Create the GraphQL object to represent the JobType in schemas/factory.go.

package schemas

import (
	"github.com/graphql-go/graphql"
)

// We can initialize Objects like this unless they need a special resolver
var jobType = graphql.NewObject(graphql.ObjectConfig{
	Name: "Job",
	Fields: graphql.Fields{
		"id": &graphql.Field{
			Type: graphql.String,
		},
		"employeeID": &graphql.Field{
			Type: graphql.String,
		},
		"company": &graphql.Field{
			Type: graphql.String,
		},
		"title": &graphql.Field{
			Type: graphql.String,
		},
		"start": &graphql.Field{
			Type: graphql.String,
		},
		"end": &graphql.Field{
			Type: graphql.String,
		},
	},
},
)
schemas/factory.go — The GraphQL object to represent jobs

Let’s begin by fixing the Field for the Jobs array. Notice how we pass the service as a parameter so we can reach the needed Resolver function.

// generateJobsField will build the GraphQL Field for jobs
func generateJobsField(gs *gopher.GopherService) *graphql.Field {
	return &graphql.Field{
		// Return a list of Jobs
		Type:        graphql.NewList(jobType),
		Description: "A list of all jobs the gopher had",
		Resolve:     gs.ResolveJobs,
	}
}
factory.go— building the jobs field which is a list of jobtypes

Now that we have the Job field done, we want to set that as an available data field for the Gopher type. We will remove the gopher.go file and move the content into its generator function, because again, we need the GopherService present. Remember that the Jobs field will be a child to Gopher, this makes the Source correct.

// genereateGopherType will assemble the Gophertype and all related fields
func generateGopherType(gs *gopher.GopherService) *graphql.Object {
	return graphql.NewObject(graphql.ObjectConfig{
		Name: "Gopher",
		// Fields is the field values to declare the structure of the object
		Fields: graphql.Fields{
			"id": &graphql.Field{
				Type:        graphql.ID,
				Description: "The ID that is used to identify unique gophers",
			},
			"name": &graphql.Field{
				Type:        graphql.String,
				Description: "The name of the gopher",
			},
			"hired": &graphql.Field{
				Type:        graphql.Boolean,
				Description: "True if the Gopher is employeed",
			},
			"profession": &graphql.Field{
				Type:        graphql.String,
				Description: "The gophers last/current profession",
			},
			// Here we create a graphql.Field which is depending on the jobs repository, notice how the Gopher struct does not contain any information about jobs
			// But this still works
			"jobs": generateJobsField(gs),
		}})
}
factory.go — Generating the GopherType, which contains a field for Jobs.

It’s time to finalize the RootQuery and the Schema in a GenerateSchema which is exported to other packages.

// GenerateSchema will create a GraphQL Schema and set the Resolvers found in the GopherService
// For all the needed fields
func GenerateSchema(gs *gopher.GopherService) (*graphql.Schema, error) {
	gopherType := generateGopherType(gs)
	// RootQuery
	fields := graphql.Fields{
		// We define the Gophers query
		"gophers": &graphql.Field{
			// It will return a list of GopherTypes, a List is an Slice
			Type: graphql.NewList(gopherType),
			// We change the Resolver to use the gopherRepo instead, allowing us to access all Gophers
			Resolve: gs.ResolveGophers,
			// Description explains the field
			Description: "Query all Gophers",
		},
	}
	rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
	// Now combine all Objects into a Schema Configuration
	schemaConfig := graphql.SchemaConfig{
		// Query is the root object query schema
		Query: graphql.NewObject(rootQuery)}
	// Create a new GraphQL Schema
	schema, err := graphql.NewSchema(schemaConfig)
	if err != nil {
		return nil, err
	}
	return &schema, nil
}
factory.go — The GenerateSchema which chains together all the pieces into a RootQuery

To implement, remove the old main function in main.go and use the newly created Repositories and Schema generator.

func main() {

	gopherService := gopher.NewService(
		gopher.NewMemoryRepository(),
		job.NewMemoryRepository(),
	)

	schema, err := schemas.GenerateSchema(&gopherService)
	if err != nil {
		panic(err)
	}

	StartServer(schema)
}
main.go — The even easier version with no domain knowledge for the main function

Restart the application and try querying for jobs. You should see that under each Gopher there is a list of jobs presented in the JSON response. Amazing right?

JSON Response for the Gophers query
JSON Response for the Gophers query

If you wonder about the job resolver and gopher resolver that we created, there is a reason why we haven’t implemented them yet. We don’t want to fetch ALL items each time, but we want to be able to query for certain Gophers only. Enter Arguments.

Arguments — Ability to specify search values

Specify certain data to query by using arguments
Specify certain data to query by using arguments

It makes sense to have a way of searching for specific data. This way is done by using Arguments in the Query. An Argument is a named value to include in the query.

A GraphQL field can have zero or many arguments available.

It’s easier to probably implement it to learn, so let’s add the ability to query specific Gophers and Jobs.

In the graphql.ResolveParams we have an Args field that will contain all the arguments sent in the query. We can use the Args field to search for any Argument we add. Let’s add an argument to the Jobs Resolver first, in which we ask for a company argument.

Update the JobRepository to accept yet another parameter which is the companyName.

// GetJobs returns all jobs for a certain Employee
func (imr *InMemoryRepository) GetJobs(employeeID, companyName string) ([]Job, error) {
	if jobs, ok := imr.jobs[employeeID]; ok {
		filtered := make([]Job, 0)
		// Filter out companyName
		for _, job := range jobs {
			// If Company Is Empty accept it, If Company matches filter accept it
			if (job.Company == companyName) || companyName == "" {
				filtered = append(filtered, job)
			}
		}
		return filtered, nil
	}
	return nil, errors.New("no such employee exist")

}
job/memory.go — GetJobs updated to get company name

We also fix the Repository to correlate the new changes.

// Repository is used to specify whats needed to fulfill the job storage requirements
type Repository interface {
	// GetJobs will search for all jobs related to and EmployeeID
	GetJobs(employeeID, company string) ([]Job, error)
}
job/job.go — Repository changes

Now let’s fix the ResolveJobs to check for the Argument.

// ResolveJobs is used to find all jobs related to a gopher
func (gs *GopherService) ResolveJobs(p graphql.ResolveParams) (interface{}, error) {
	// Fetch Source Value
	g, ok := p.Source.(Gopher)

	if !ok {
		return nil, errors.New("source was not a Gopher")
	}
	// Here we extract the Argument Company
	company := ""
	if value, ok := p.Args["company"]; ok {
		company, ok = value.(string)
		if !ok {
			return nil, errors.New("id has to be a string")
		}
	}

	// Find Jobs Based on the Gophers ID
	jobs, err := gs.jobs.GetJobs(g.ID, company)
	if err != nil {
		return nil, err
	}
	return jobs, nil
}
gopher/resolver.go — The Resolver now accepts argument Company

The last thing we need to do is make the graphql.Field also accepts the argument. Each graphql.Field has an Args that can be used to define the possible arguments, Args is a map[string]*ArgumentConfig in where we have to set the name and the data type. Let’s add it to the schemas/factory


// generateJobsField will build the GraphQL Field for jobs
func generateJobsField(gs *gopher.GopherService) *graphql.Field {
	return &graphql.Field{
		// Return a list of Jobs
		Type:        graphql.NewList(jobType),
		Description: "A list of all jobs the gopher had",
		Resolve:     gs.ResolveJobs,
		// Args are the possible arguments.
		Args: graphql.FieldConfigArgument{
			"company": &graphql.ArgumentConfig{
				Type: graphql.String,
			},
		},
	}
}
schemas/factory — The jobs field now has an argument

Now you can request certain companies by using an argument in the query. The arguments are added by using the following syntax after the Field.

jobs(company: "value"){
The image shows how we can filter for certain Jobs by using and Argument
The image shows how we can filter for certain Jobs by using and Argument

Mutations — Modify the Data

Mutations in GraphQL allows changes to data
Mutations in GraphQL allows changes to data

Great, we can query data from the server. What about when we want to modify data?

That is when we have mutations in GraphQL. A mutation is defined by a RootMutation just as we had a RootQuery. So we need to build a schema for all available mutations, their arguments, and fields available.

A Mutation in GraphQL will look very much like a Query, and in fact, it will also return the result. So a Mutation can be used to apply new values, but also to fetch values after they are applied. Just as a Query you defined what values to return.

Let’s allow JobRepository to GetJob by an ID and to Update the job, this is so we can later create a Mutation to modify the start and end date of a job.

// Repository is used to specify whats needed to fulfill the job storage requirements
type Repository interface {
	// GetJobs will search for all jobs related to and EmployeeID
	GetJobs(employeeID, company string) ([]Job, error)
	// GetJob will search for a certain job based on ID
	GetJob(employeeID, jobid string) (Job, error)
	// Update will take in a job and update the repository, it will return the new state of the job
	Update(Job) (Job, error)
}
job/job.go — Update the repository with two new functions

Then open the memory.go and update the storage solution to handle these new functions.

// GetJob will return a job based on the ID
func (imr *InMemoryRepository) GetJob(employeeID, jobID string) (Job, error) {
	if jobs, ok := imr.jobs[employeeID]; ok {
		for _, job := range jobs {
			// If Company Is Empty accept it, If Company matches filter accept it
			if job.ID == jobID {
				return job, nil
			}
		}
		return Job{}, errors.New("no such job exists for that employee")
	}
	return Job{}, errors.New("no such employee exist")
}

// Update will update a job and return the new state of it
func (imr *InMemoryRepository) Update(j Job) (Job, error) {
	imr.Lock()
	defer imr.Unlock()
	// Grab the employees jobs and locate the job and change the value
	if jobs, ok := imr.jobs[j.EmployeeID]; ok {
		// Find correct job
		for i, job := range jobs {
			if job.ID == j.ID {
				// Replace the whole instance by index
				imr.jobs[j.EmployeeID][i] = j
				// Return Job, we can Image changes from input Job, like CreateJob which will generate an ID etc etc.
				return j, nil
			}
		}
	}
	return Job{}, errors.New("no such employee exist")
}
job/memory.go — In memory solution to handle job updates

Next, we need to add a Resolver function to the GopherService, one thing I’ve noticed with graphql-go is that there is quite the overhead since we deal with a lot of interface{}. You can avoid much overhead by creating helper functions to deal with the type assertions, in the code snippet below you see a grabStringArgument which is used to extract GraphQL Arguments that are strings. The mutation resolver is just like the query resolver so there isn’t anything new here.

// MutateJobs is used to modify jobs based on a mutation request
// Available params are
// employeeid! -- the id of the employee, required
// jobid! -- job to modify, required
// start -- the date to set as start date
// end -- the date to set as end
func (gs *GopherService) MutateJobs(p graphql.ResolveParams) (interface{}, error) {
	employee, err := grabStringArgument("employeeid", p.Args, true)
	if err != nil {
		return nil, err
	}
	jobid, err := grabStringArgument("jobid", p.Args, true)
	if err != nil {
		return nil, err
	}
	start, err := grabStringArgument("start", p.Args, false)
	if err != nil {
		return nil, err
	}
	end, err := grabStringArgument("end", p.Args, false)
	if err != nil {
		return nil, err
	}

	// Get the job
	job, err := gs.jobs.GetJob(employee, jobid)
	if err != nil {
		return nil, err
	}
	// Modify start and end date if they are set
	if start != "" {
		job.Start = start
	}

	if end != "" {
		job.End = end
	}
	// Update with new values
	return gs.jobs.Update(job)
}

// grabStringArgument is used to grab a string argument
func grabStringArgument(k string, args map[string]interface{}, required bool) (string, error) {
	// first check presense of arg
	if value, ok := args[k]; ok {
		// check string datatype
		v, o := value.(string)
		if !o {
			return "", fmt.Errorf("%s is not a string value", k)
		}
		return v, nil
	}
	if required {
		return "", fmt.Errorf("missing argument %s", k)
	}
	return "", nil
}
gopher/resolver.go — Added a mutation resolver which updates a job.

Next, we need to update the Schema to have a mutation. Creating all the graphql.Fields can become quite much code as you might have noticed, so I usually create a generator function that reduces the code duplication by a factor of a hella lot.

To create a field we need a Type, which is anything that fulfills an Output interface which looks like the following snippet

type Output interface {
    Name() string
    Description() string
    String() string
    Error() error
}

The second parameter is the Resolver which is an alias FieldResolveFn func(p ResolveParams) (interface{},error), the third is a string description, and the fourth a map of arguments to allow.

// generateGraphQLField is a generic builder factory to create graphql fields
func generateGraphQLField(output graphql.Output, resolver graphql.FieldResolveFn, description string, args graphql.FieldConfigArgument) *graphql.Field {
	return &graphql.Field{
		Type:        output,
		Resolve:     resolver,
		Description: description,
		Args:        args,
	}
}
schemas/factory.go — The generic factory for graphQL fields

Time to build the mutation query, create a mutation.go file in the schemas package. We begin by creating the arguments available in the request. We want the mutation request to require two arguments, so we create them using graphql.NewNonNull function. Using NewNonNull will make the GraphQL server trigger an error if a request with empty values is sent.

package schemas

import (
	"github.com/graphql-go/graphql"
	"github.com/programmingpercy/gopheragency/gopher"
)
// modifyJobArgs are arguments available for the modifyJob Mutation request
var modifyJobArgs = graphql.FieldConfigArgument{
	"employeeid": &graphql.ArgumentConfig{
		// Create a string argument that cannot be empty
		Type: graphql.NewNonNull(graphql.String),
	},
	"jobid": &graphql.ArgumentConfig{
		Type: graphql.NewNonNull(graphql.String),
	},
	// The new start date to apply if set
	"start": &graphql.ArgumentConfig{
		Type: graphql.String,
	},
	// The new end date to apply if set
	"end": &graphql.ArgumentConfig{
		Type: graphql.String,
	},
}
schemas/mutation.go — The arguments available for the Mutatejob request.

We need to create the RootMutation with the new mutation in it, just as the query was created. We will now be using the generateGraphQLField to make the code a lot shorter.

// generateRootMutation will create the root mutation object
func generateRootMutation(gs *gopher.GopherService) *graphql.Object {

	mutationFields := graphql.Fields{
		// Create a mutation named modifyJob which accepts a JobType
		"modifyJob": generateGraphQLField(jobType, gs.MutateJobs, "Modify a job for a gopher", modifyJobArgs),
	}
	mutationConfig := graphql.ObjectConfig{Name: "RootMutation", Fields: mutationFields}

	return graphql.NewObject(mutationConfig)
}
schemas/mutation.go — Building the root mutation

The last thing to do before we can try the mutation is to apply the RootMutation to the Schema in factory.go

// GenerateSchema will create a GraphQL Schema and set the Resolvers found in the GopherService
// For all the needed fields
func GenerateSchema(gs *gopher.GopherService) (*graphql.Schema, error) {
	gopherType := generateGopherType(gs)
	// RootQuery
	fields := graphql.Fields{
		// We define the Gophers query
		"gophers": &graphql.Field{
			// It will return a list of GopherTypes, a List is an Slice
			Type: graphql.NewList(gopherType),
			// We change the Resolver to use the gopherRepo instead, allowing us to access all Gophers
			Resolve: gs.ResolveGophers,
			// Description explains the field
			Description: "Query all Gophers",
		},
	}
	rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
	// build RootMutation 
	rootMutation := generateRootMutation(gs)

	// Now combine all Objects into a Schema Configuration
	schemaConfig := graphql.SchemaConfig{
		// Query is the root object query schema
		Query: graphql.NewObject(rootQuery),
		// Appliy the Mutation to the schema
		Mutation: rootMutation,
	}
	// Create a new GraphQL Schema
	schema, err := graphql.NewSchema(schemaConfig)
	if err != nil {
		return nil, err
	}
	return &schema, nil
}
schemas/factory.go — Adding the root mutation to the Schema Config.

Open GraphiQL to try it out. We send the Mutation in the same way as the Query, simply replace the Keyword.

GraphiQL — Sending a Mutation request to change the start date
GraphiQL — Sending a Mutation request to change the start date

Conclusion

We have covered the very core of GraphQL. During the article you have learned how to Query data, Mutate it and the basics of how GraphQL works. You have seen how we can combine data sources to a single output and how to structure the project.

GraphQL has grown a lot and has much more to offer. Here are some topics you should investigate by yourself to increase your knowledge.

  • Enumerations
  • Interfaces, Fragments and Inline Fragments
  • Subscriptions

Will you be using GraphQL or not?

You can find the full code on GitHub.

Thanks for reading and feel free to reach out to me in any of my social media found below.

If you enjoyed my writing, please support future articles by buying me an Coffee

Sign up for my Awesome newsletter