Setting Up gqlgen
Table of Contents
Now that we have an application scaffolded with Cobra we can set up gqlgen. This library is great because it will allow us to create our GraphQL schema files and generate the scaffolding for the parts of the app that need code to resolve them. It’s super developer friendly so don’t worry if it doesn’t make sense yet.
Since we’re going to be touching on our first GraphQL concepts here, if you’re not already familiar with GraphQL or just need a refresh, I recommend taking a look at the Introduction to GraphQL. This will help you to understand the concepts of the GraphQL language and how it’s going to be relevant to you. However, I’ll tend to keep the concepts within GraphQL to a minimum.
The first thing we need to do is add the tools that are used to our project via a pattern were tools get included as
development dependencies through a tools.go
file at the root of the project. Create the tools.go
file and put the
following in it:
//go:build tools
package tools
import (
_ "github.com/99designs/gqlgen"
_ "github.com/99designs/gqlgen/graphql/introspection"
)
Let’s break down what’s in this file. First we put a comment to define the Go build environment. In this case we set it
to tools so that (by default when building the app) this file isn’t included in the build. Next we setting the
package for this file. Since we aren’t including it in the build we can call it whatever we want (so don’t worry that
it doesn’t match the root-level package
in main.go
). Finally, we just need to use the _
(blank identifier) for
the packages since we don’t actually use them.
Doing all of this will allow us to keep our tools versioned and managed through our go.mod
and go.sum
files. Now
that we have that file created we can add them to the go.mod
by running:
go mod tidy
With the tools added, we can now initialize gqlgen
in our project. In a terminal session from the root of the project
run the following command to generate the initial resources:
go run github.com/99designs/gqlgen init
A handful of files will be created to get us started. However, since we’re managing our app from Cobra we are going to
delete the server.go
file that was created at the top-level of our project. We will be reimplementing what’s in there
within the cmd/api.go
file. Feel free to take a look at the contents of it to see how our version will be similar to
the default one.
I’m fairly particular about the structure of my projects so I am going to modify the generation settings by editing the
gqlgen.yml
:
schema:
- gql/schema/*.graphql
exec:
filename: gql/generated.go
package: gql
model:
filename: gql/model/models_gen.go
package: model
resolver:
layout: follow-schema
dir: gql
package: gql
filename_template: "{name}.go"
omit_template_comment: false
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Int:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
I’m making a handful of changes that make it a bit cleaner. This is all personal preference so don’t feel like you need to follow this:
- I prefer all of my top-level packages to be three letters (
gql
in our case). It’s not a requirement by any means, it’s just a convention that the community has tended to adopt. - Keeping my schema files in their own directory
- Split up my schema files by their top level resource (project, task, note, etc.) with one common file.
With the config file updated, we can delete the old graph
directory. We don’t need it as the content within our new
gql
directory will be regenerated when we run the generate command.
Let’s create our first schema file. We’ll create the common schema (gql/schema/common.graphql
) with a “hello world”
example:
type Query {
hello(name: String!): String!
}
This file will contain all the definitions that aren’t tied to a specific resource and are generally used in many of the schema files. For now though, we’re just going to create the handler to query our GraphQL instance a “hello world” example.
Now that we’ve updated our schema file we can generate our code again:
go run github.com/99designs/gqlgen generate
I prefer that VS Code not show me generated code in the file explorer (when you go to a definition it will still open
that file and show you what you’re looking for). Create the .vscode/settings.json
file with the following:
{
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
"gql/{generated,model/*_gen}.go": true
}
}
files.exclude
. The other entries in this are all excluded in VS Code
by default.Before we commit our changes, we’re going to add the generated code that we don’t change (more on that later) to a
.gitignore
file:
gql/model/*_gen.go
gql/generated.go
To get our GraphQL server to a usable state, we need to modify the gql/resolver.go
file:
package gql
import "github.com/99designs/gqlgen/graphql/handler"
//go:generate go run github.com/99designs/gqlgen generate
type Resolver struct{}
func NewResolver() Config {
return Config{
Resolvers: &Resolver{},
}
}
func NewServer() *handler.Server {
return handler.NewDefaultServer(NewExecutableSchema(NewResolver()))
}
For now, we are just keeping the code more self-contained within the gql
package. This will later provide us the place
where we can inject dependencies into the resolver methods that we add to the GraphQL API.
Next up we need to set up the GraphQL server to run within our API command. Modify the cmd/api.go
to create our
GraphQL server:
package cmd
import (
"net/http"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/go-chi/chi/v5"
"github.com/spf13/cobra"
"github.com/nikkomiu/gentql/gql"
)
var apiCmd = &cobra.Command{
Use: "api",
Short: "Start the API services for gentql",
RunE: runAPI,
}
func init() {
rootCmd.AddCommand(apiCmd)
}
func runAPI(cmd *cobra.Command, args []string) error {
router := chi.NewRouter()
srv := gql.NewServer()
router.Handle("/graphql", srv)
router.Handle("/graphiql", playground.Handler("GentQL", "/graphql"))
err := http.ListenAndServe(":8080", router)
if err != nil {
return err
}
return nil
}
Since we added Chi for the HTTP router, we need to update our dependencies:
go mod tidy
We can start our API now and test our hello(name)
query. Run the following from your terminal:
go run . api
Navigate to the GraphQL UI endpoint (http://localhost:8080/graphiql) where we can interact with our GraphQL API directly within a browser. We can now query for our hello method with the following:
query {
hello(name: "Joe")
}
If we execute this we should get back something like:
{
"errors": [
{
"message": "internal system error",
"path": ["hello"]
}
],
"data": null
}
This error is coming from our resolver method. When we generated our code there was a method created for us to query
for the hello(name)
. Currently, this method is set to cause the app to panic()
but the GraphQL server will properly
catch the panic within our resolver to keep it contained to just the single request.
Open the gql/common.go
file and update the Hello(context.Context, string)
method:
// Hello is the resolver for the hello field.
func (r *queryResolver) Hello(ctx context.Context, name string) (string, error) {
return fmt.Sprintf("Hello, %s!", name), nil
}
This way we will return a string (and no error) from our Hello(context.Context, string)
method. Now, restart the app
and re-run the query in your browser to get back the response we were hoping for:
{
"data": {
"hello": "Hello, Joe!"
}
}
Chi includes middleware with it. Some of them are useful to have enabled by default. Let’s add them to our API now:
router := chi.NewRouter()
router.Use(
middleware.RequestID,
middleware.RealIP,
middleware.Logger,
middleware.Recoverer,
)
srv := gql.NewServer()
//...
If you get an error on importing chi
, make sure the middleware
import has v5
in it. If it doesn’t fix the import
manually:
import (
"net/http"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
// ...
If you notice, the common.go
schema file contains a comment at the top which says that the file is auto generated.
// 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.
// Code generated by github.com/99designs/gqlgen version v0.17.49
On GitHub this may cause the web-based diff viewer to see these files as auto-generated files that don’t need review
when creating PRs. Since this isn’t actually the case for these files, let’s tell gqlgen to stop generating these
comments since they contain implementation logic for our resolvers. Add the following to the gqlgen.yml
:
omit_gqlgen_file_notice: true
And regenerate the code:
go generate ./...
You should now have your GraphQL API endpoints created and wired into your app CLI. We also explored how Go methods get generated for us with the request and response properties automatically mapped to function arguments and return parameters.
Next up, we’re going to add ent
to manage our database layer for us using code-generation as well.