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!
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++.
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
To install gqlgen as a dependency, run the following two commands:
go get github.com/99designs/gqlgen go run github.com/99designs/gqlgen init
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
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! }
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! }
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
routesmutations
: Used for creating, updating, and deleting data, equivalent to REST
, POST
, and PUT
type Query { todos: [Todo!]! } type Mutation { createTodo(input: NewTodo!): Todo! }
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 }
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"` }
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 }
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 }
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 }
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
.
Now, let’s test our API. Run the server with the command below:
go run server.go
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 } }
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 } }
Go ahead and add a few to-do items, then try again to get those items with the query.
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 }
Then, run the command below:
go run github.com/99designs/gqlgen generate
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!
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!
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.Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.