October 6, 2021

Using GraphQL with Golang

Cathleen Turner
@_cat_turner
BackendGolangGraphQL
Last updated October 5, 2021

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.

Prerequisites

To get the most out of the tutorial, I recommend that you first:

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

Our heroes: Sarah Cameron, JJ, Pope, John B, and Kiara

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 file schema.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 in schema.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.

You can learn more about mutations via the Apollo docs.

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.

You can learn more about GraphQL Input Types via the Apollo docs.

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

Written by

Cathleen Turner

Follow

Stay in our orbit!

Become an Apollo insider and get first access to new features, best practices, and community events. Oh, and no junk mail. Ever.

Make this article better!

Was this post helpful? Have suggestions? Consider so we can improve it for future readers ✨.

Similar posts

October 19, 2021

Demystifying GraphQL Misconceptions | Top Five GraphQL Myths Debunked

by Kurt Kemple

Company