Using GraphQL with Golang
Cathleen Turner
Golang is a fast and modern programming language that helps you build simple, reliable, and efficient software. If you’ve been wondering how to build a backend API with GraphQL and Golang, you’ll be pleased to know that there are some great community-supported libraries that help you do just that.
In this tutorial, we’ll learn how to use the gqlgen library to build a simple GraphQL API with Golang.
Golang GraphQL Prerequisites
To get the most out of the tutorial, I recommend that you first:
- Have a basic understanding of Golang
- Know the basics of GraphQL (see Odyssey to learn GraphQL with interactive tutorials)
- Have your dev environment set up to write Golang code (see the official docs)
gqlgen: a schema-first approach to building GraphQL APIs
In building out a GraphQL server, one of the first choices you have to make is to decide whether you want to build the server in a code-first way or in a schema-first way.
The code-first approach to GraphQL API development is to construct your schema through the use of library APIs. The schema-first approach means that you write your schema manually using the GraphQL schema definition language.
While there are benefits and drawbacks to each approach, Apollo GraphQL recommends (via their Principled GraphQL guide) to make use of the schema-first approach. One advantage of this approach is that it allows you to focus on defining your data requirements based on the types and operations your clients actually need. That is, you focus on what you need first and figure out how you’ll do it later. This architectural decision means that the schema is the single source of truth.
In the Golang community, the schema-first GraphQL library we’ll use is called gqlgen. You can read their docs here.
What we’re building
We will build a character API for Outer Banks, a show set in North Carolina where a group of friends hunt for lost treasure and dodge the bad guys.
Our trivial API makes the character information easy to access. By the end of this tutorial, we’ll have built a GraphQL API that can handle both queries and mutations on Outer Banks characters.
You can find the complete code for this post on GitHub.
Getting started
Let’s get started by installing gqlgen and initializing our project. We can fetch the library using the following command.
go get github.com/99designs/gqlgen
Next, add gqlgen to your project’s tools.go
.
printf '// +build tools\npackage tools\nimport _ "github.com/99designs/gqlgen"' | gofmt > tools.go
go mod tidy
Then initialize gqlgen config and generate the models.
go run github.com/99designs/gqlgen init
Finally, start the GraphQL server.
go run server.go
You should go to the URL shown in your console (which is likely to be localhost:8080
) and view the schema in GraphQL Playground.
You’ll notice that there is some schema boilerplate in place. You’ll see the types and fields that make up the schema when you click the Schema
tab on the right-hand side.
This schema is defined in schema.graphqls
.
At this point, you should see various files and folders under the directory graphql
. The directory graph its artifacts were generated by gqlgen after you typed the init
command. Some key points to learn here:
model/model_gen.go
: this is file with structs generated by gqlgen, defined by the schema fileschema.graphqls
generated/generated.go
: this is a file with generated code that injects context and middleware for each query and mutation.
You should not modify either of those files since they will be modified by gqlgen as you update your schema. Instead, you should pay attention to the following files:
schema.graphqls
: a GraphQL schema file where types, queries, and mutations are defined. The schema file uses schema-definition-language (SDL) to describe data types and operations (queries/mutations) in a human-readable way.schema.resolvers.go
: a go file with wrapper code for queries and mutations defined inschema.graphqls
Go into your schema.graphqls
file and delete everything. We will be defining our schema from scratch.
Defining queries
Queries describe how we fetch data from our API. We will define ours in schema.graphqls
.
To learn more about query types, you can read the Apollo docs.
We want to be able to find out a couple of things from our Outer Banks API – mainly, who are pogues and kooks (the two types of factions in the show). We will also need the ability to fetch by id.
type Query {
character(id:ID!): Character
pogues: [Character]!
kooks: [Character]!
}
So far, there is only one type we’ll be returning: the Character
type. Before we define this, let’s handle the mutation definition.
Defining mutations
Mutations can be used to perform actions like adding characters into our database.
We can define a mutation in the same schema.graphqls
file. The mutation will also return the Character
that was created.
type Mutation {
upsertCharacter(input: CharacterInput!): Character!
}
You’ll notice here that we also have an Input Type. This type is responsible for handling the input needed to create or modify a character.
Defining the types
We now define the types because we know what data we will need to use as inputs and outputs for our GraphQL queries and mutations.
The Character
type should have the following defining characteristics: a unique id
field and the name
.
type Character {
id: ID!
name: String!
}
input CharacterInput {
name: String!
id: String
}
Note that CharacterInput
is a distinct GraphQL type. This type is only used for inputs in queries and/or mutations.
Generating code and running the API
We will now generate code, which will update the following files using the information we provided in the schema file:
- schema.resolvers.go
- model/models_gen.go
- generated/generated.go
Delete the example code in schema.resolvers.go
and then run the following command:
go run github.com/99designs/gqlgen generate
After running this command, if you run git diff
, you will see that these files have been updated.
Let’s next confirm that the API can still run by running this command:
go run server.go
If you try to run a simple query, you should run into an error. This is because we haven’t defined the resolver code yet. Let’s do that next.
Defining the backend to fetch and store values
We will now define the resolvers so that our backend can store and fetch characters. Since the boilerplate code has already been written by gqlgen, all we need to do is write the Golang code to store and fetch values.
To keep things simple, let’s build a way to store our characters in memory.
To support the scenarios where we want to create or update a new character, we can use the same mutation. To create, when we exclude a character id, the API will assume this is a new character, it’ll create it, and then save it in the map. When you include the id, the code will assume you are updating a character that has already been saved on the map.
In resolver.go
, add a map that we will use to store characters.
type Resolver struct {
CharacterStore map[string]model.Character
}
In schema.resolvers.go
, modify the UpsertCharacter
method.
func (r *mutationResolver) UpsertCharacter(ctx context.Context, input model.CharacterInput) (*model.Character, error) {
id := input.ID
var character model.Character
character.Name = input.Name
n := len(r.Resolver.CharacterStore)
if n == 0 {
r.Resolver.CharacterStore = make(map[string]model.Character)
}
if id != nil {
_, ok := r.Resolver.CharacterStore[*id]
if !ok {
return nil, fmt.Errorf("not found")
}
r.Resolver.CharacterStore[*id] = character
} else {
// generate unique id
nid := strconv.Itoa(n + 1)
character.ID = nid
r.Resolver.CharacterStore[nid] = character
}
return &character, nil
}
Test the mutation by creating a character. Go to your tab where GraphQL Playground is open and type the mutation on the left-hand side.
Next, let’s add code to access the character from the hashmap. In schema.resolvers.go
, modify the Character
method.
func (r *queryResolver) Character(ctx context.Context, id string) (*model.Character, error) {
character, ok := r.Resolver.CharacterStore[id]
if !ok {
return nil, fmt.Errorf("not found")
}
return &character, nil
}
Test the query.
Modifying the schema
We have confirmed that our queries and mutations work. Updating the schema is as easy as creating it because we do it schema-first. Because we modify the schema, we are practicing schema-first development.
We would like to know two additional pieces of information: If a character is a pogue or a kook. And if they are a hero (main character).
We can do this quickly by modifying the schema in schema.graphqls
.
To define a character type (pogue or kook), let’s use an Enum Type Called CharacterType. Although there are only two types, they may get new character types in later seasons. The field hero can be a boolean.
enum CliqueType {
"People who are elite with parents having money"
KOOKS
"People who desperate to move up the social ladder to become new versions of themselves and establish new beginnings"
POGUES
}
type Character {
id: ID!
name: String!
isHero: Boolean!
cliqueType: CliqueType!
}
input CharacterInput {
name: String!
id: String
isHero: Boolean
cliqueType: CliqueType!
}
Re-generating the code
Run the command below. You will notice that files are regenerated, but the code you wrote earlier has not been erased.
go run github.com/99designs/gqlgen generate
When you open your model files, you should see the addition of the fields cliqueType
and isHero
on both the Character
type and CharacterInput
input type. Do note that a new struct was created, called CliqueType
.
Thinking about running the API? Not so fast! You will need to update the UpsertCharacter
method so that we can include these changes.
func (r *mutationResolver) UpsertCharacter(ctx context.Context, input model.CharacterInput) (*model.Character, error) {
id := input.ID
var character model.Character
character.Name = input.Name
character.CliqueType = input.CliqueType
n := len(r.Resolver.CharacterStore)
if n == 0 {
r.Resolver.CharacterStore = make(map[string]model.Character)
}
if id != nil {
cs, ok := r.Resolver.CharacterStore[*id]
if !ok {
return nil, fmt.Errorf("not found")
}
if input.IsHero != nil {
character.IsHero = *input.IsHero
} else {
character.IsHero = cs.IsHero
}
r.Resolver.CharacterStore[*id] = character
} else {
// generate unique id
nid := strconv.Itoa(n + 1)
character.ID = nid
if input.IsHero != nil {
character.IsHero = *input.IsHero
}
r.Resolver.CharacterStore[nid] = character
}
return &character, nil
}
Add characters with these new fields – friends
and isHero
(boolean). Restart the API and try to create a new Character
with these two new fields.
When you enter a cliqueType
in the query variables, you will see your enum options appear. If you check your schema (click on the tab on the right-hand side, you will see that Character
and CharacterInput
types have the fields isHero
and cliqueType
.
While storing different characters is useful, we want a quick way to pull characters by type. In the past, we had two queries. I think we should change direction and instead do one query where the clique type is an input.
Do we need to delete the Golang code generated for these queries (kooks, pogues) that we defined in the schema? Nope! Just update the schema, and regenerate your code.
Remove two queries and add the new query called characters I had just described.
type Query {
character(id:ID!): Character
characters(cliqueType:CliqueType!): [Character!]
}
When you do git diff
in your terminal, you will see that the old generated code for the kooks and pogues query was removed for you.
You will notice a new section of code that looks like this:
That is OK. The code generator moves previously defined methods. Feel free to remove that code, since it’s not doing anything.
I think one of the most powerful features of using Graphql is how easy it can be for you to make schema changes with minimal consequences. This is important when you are iterating on a new product and you need to adapt to customer needs. By designing the schema first, and making changes to the schema as you build your GraphQL API, you will not waste any time writing unused queries or template code. You can instead focus on the new, more useful queries and delete old queries from the schema that no longer serve you.
Define the method characters so that you can filter by CliqueType
.
func (r *queryResolver) Characters(ctx context.Context, cliqueType model.CliqueType) ([]*model.Character, error) {
characters := make([]*model.Character, 0)
for idx := range r.Resolver.CharacterStore {
character := r.Resolver.CharacterStore[idx]
if character.CliqueType == cliqueType {
characters = append(characters, &character)
}
}
return characters, nil
}
Test the query.
Conclusion
We have just learned how to use GraphQL with Golang to create an API that defines characters in Outer Banks and who our heroes can trust. We were able to generate boilerplate code for our queries and mutations by modifying one schema file, schema.graphqls
. Our example, though simple, shows the power we can have when we develop using gqlgen by making changes to the schema before we modify the code. Because we think of the schema first, we can quickly define the API’s business requirements through code.
Check out the repo we walked through in this tutorial for a complete view of the code.
https://github.com/cat-turner/Outer Banks-api