DEV Community

Cover image for gqlgen: Build a faster GraphQL server
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

gqlgen: Build a faster GraphQL server

Written by Alex Merced✏️

GraphQL is a query language and runtime created by Facebook as an alternative to using REST APIs. Go is a lower-level, compiled programming language created by Google that has become quite popular for creating fast-running APIs.

GraphQL and Go each offer unique advantages when writing APIs. In this article, we’ll explore gqlgen, which helps you to easily write GraphQL APIs using Go, combining the best of both technologies.

First, we’ll look into the advantages of using a tool like gqlgen, covering the beneficial features of both GraphQL and Go. Then, we’ll learn how to get started with gqlgen by building a simple to-do list application. Let’s get started!

Inner workings of GraphQL and Go

In GraphQL, instead of making requests with different HTTP verbs to different URLs, all requests are made as POST requests to a single URL. In the POST request, a string is sent in the body expressing what query or mutation to run and what properties to return.

GraphQL APIs are self-documenting, so when using tools like GraphQL or GraphQL Playground, you can easily see all the details of the API, almost like Swagger for REST APIs, but built-in.

On the other hand, Go can provide faster performance than higher-level languages like JavaScript, Python, and Ruby, as well as an easier syntax and toolchain than using C or C++.

Getting started with gqlgen

To follow along with this tutorial, you’ll need to have Go installed. At the time of writing, I am running Go v1.17.4.

Open up VS Code or your preferred IDE to an empty folder, then open up the terminal. Create a new Go module. I named mine my/graphql/api, but you can use any name you wish:

go mod init my/graphql/api
Enter fullscreen mode Exit fullscreen mode

To install gqlgen as a dependency, run the following two commands:

go get github.com/99designs/gqlgen

go run github.com/99designs/gqlgen init
Enter fullscreen mode Exit fullscreen mode

The code above will generate the following file structure:

├── go.mod
├── go.sum
├── gqlgen.yml               - For Configuration
├── graph
   ├── generated            - The Generated Runtime
      └── generated.go
   ├── model                - For any models and database connections
      └── models_gen.go
   ├── resolver.go          - Write all your resolvers here
   ├── schema.graphqls      - Your Schema
   └── schema.resolvers.go  - the resolver implementation for schema.graphql
└── server.go                - The entry point to your app. Customize it however you see fit
Enter fullscreen mode Exit fullscreen mode

Setting up our to-do list

In schema.graphqls, you'll see the default code below, which contains an example to-do list:

# GraphQL schema example
#
# https://gqlgen.com/getting-started/

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
}

type Query {
  todos: [Todo!]!
}

input NewTodo {
  text: String!
  userId: String!
}

type Mutation {
  createTodo(input: NewTodo!): Todo!
}
Enter fullscreen mode Exit fullscreen mode

Let's break this down. Each data type in your API should get type annotations like the following:

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}
Enter fullscreen mode Exit fullscreen mode

The exclamation points notate fields that are required. You can also create input types that are used as arguments to your resolvers. In a REST API, these are the equivalent to controller action functions. An input type looks like the code below, which we can then use in our resolver types:

input NewTodo {
  text: String!
  userId: String!
}

We also must declare all resolvers, which fall into two categories:

  • query: Used for getting information, equivalent to REST API GET routes
  • mutations: Used for creating, updating, and deleting data, equivalent to REST, POST, and PUT
type Query {
  todos: [Todo!]!
}

type Mutation {
  createTodo(input: NewTodo!): Todo!
}
Enter fullscreen mode Exit fullscreen mode

We’ll declare all possible queries in the Query type, and the mutations will go in the Mutation type. Essentially, Query and Mutation are two lists of function signatures. Notice that we need to provide an input to the createTodo resolver in the form of an argument called input, which must match the NewTodo input type.

By defining the schema in this manner, GraphQL is self-documenting, knowing which functions to run when different requests come in. However, we do need to define resolver functions to go with the resolver declarations that will be found in schema.resolvers.go:

package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
        "context"
        "fmt"
        "my/graphql/api/graph/generated"
        "my/graphql/api/graph/model"
)

func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
        panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
        panic(fmt.Errorf("not implemented"))
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Enter fullscreen mode Exit fullscreen mode

Notice that the resolver functions already exist, which is one of gqlgen’s highlights. Once you've written your schemas, you can just add the corresponding models in the models_gen.go file as follows:

// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.

package model

type NewTodo struct {
        Text   string `json:"text"`
        UserID string `json:"userId"`
}

type Todo struct {
        ID   string `json:"id"`
        Text string `json:"text"`
        Done bool   `json:"done"`
        User *User  `json:"user"`
}

type User struct {
        ID   string `json:"id"`
        Name string `json:"name"`
}
Enter fullscreen mode Exit fullscreen mode

When you run the command go run github.com/99designs/gqlgen generate, it uses these two inputs to generate the boilerplate for the resolvers, so you can focus on the implementation.

In the resolver.go file, we can add properties to the Resolver struct, which becomes available via the resolver instance represented by r. We'll add a property that is an array of to-do items, which we can use to track the to-do items that have been created:

package graph

import "my/graphql/api/graph/model"

// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.

type Resolver struct{
        todos []*model.Todo
}
Enter fullscreen mode Exit fullscreen mode

Now, let's implement those resolver functions in schema.resolvers.go:

package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
        "context"
        "my/graphql/api/graph/generated"
        "my/graphql/api/graph/model"
        "strconv"
)

func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {

        id := strconv.Itoa(len(r.todos))

        // CREATE A NEW TODO
        todo := &model.Todo{
                Text:   input.Text,
                ID:     id,
                User: &model.User{ID: input.UserID, Name: "user " + input.UserID},
        }
        // ADD THE TODO TO THE TODOS ARRAY
        r.todos = append(r.todos, todo)
        // RETURN THE TODO
        return todo, nil
}

func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
        // RETURN ALL THE TODOS
        return r.todos, nil
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Enter fullscreen mode Exit fullscreen mode

You may be wondering about the following code:

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Enter fullscreen mode Exit fullscreen mode

Essentially, the code above creates an instance of queryResolver and mutationResolver. Both inherit from the Resolver struct we saw earlier, making the array available to all the resolvers. Our resolvers become methods for these structs. In short, a single instance of Resolver is created, which is then passed to an instance of queryResolver and mutationResolver.

Testing our API

Now, let’s test our API. Run the server with the command below:

go run server.go
Enter fullscreen mode Exit fullscreen mode

Go to localhost:8080, and you'll be able to access GraphQL Playground, a tool for testing GraphQL APIs. Click on docs to see the self-documentation I mentioned earlier.

The following query will bring up our to-do items, which are empty at the moment:

{
  todos{
    id
    text
    done
  }
}
Enter fullscreen mode Exit fullscreen mode

An interesting feature of GraphQL is that you don’t have to receive every field; you can specify which fields you want in the query. Below is an example of a mutation that will add a to-do item:

# I Specify that it's a mutation
mutation {
  # I invoke the createTodo mutation and pass it the input
  createTodo(input: {
    text: "This is a new todo"
    userId: "alex"
  })
  # Specify which properties I want from the return value
  {
    id
    text
    done
  }
}
Enter fullscreen mode Exit fullscreen mode

Go ahead and add a few to-do items, then try again to get those items with the query.

Testing gqlgen generate

Next, let's test out the generation feature by adding a model and seeing it auto-generate all the necessary code. Go ahead and update your schema as follows:

# GraphQL schema example
#
# https://gqlgen.com/getting-started/

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type Dog {
  id: ID!
  name: String
}

type User {
  id: ID!
  name: String!
}

type Query {
  todos: [Todo!]!
  dogs: [Dog!]!
}

input NewTodo {
  text: String!
  userId: String!
}

input NewDog {
  name: String!
}

type Mutation {
  createTodo(input: NewTodo!): Todo!
  createDog(input: NewDog!): Dog
}
Enter fullscreen mode Exit fullscreen mode

Then, run the command below:

go run github.com/99designs/gqlgen generate
Enter fullscreen mode Exit fullscreen mode

You'll notice that the models and resolvers have been auto-generated. gqlgen can help you roll out a lot of the boilerplate if you just write out your schema. How awesome is that!

Conclusion

In this article, we covered creating GraphQL APIs with gqlgen, which allows us to seamlessly write a GraphQL API using Go without writing excessive boilerplate code.

By combining the benefits of GraphQL, like self-documentation, with the speed and straightforward syntax of Go, we’ve built a faster and more performant application. I hope you enjoyed this article!


Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.

LogRocket Mission Controlhttps://logrocket.com/signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.

Top comments (0)