Building a GraphQL API in Go using gqlgen
Tags: Go, Gqlgen, GraphQL

GraphQL gives us a great framework to document an API contract (using schema and types) and gqlgen is a Go library that helps one generate a type-safe API server for that contract, relatively easily. You create your GraphQL schema, run gqlgen on it to auto-generate all the Go bindings and boilerplate you need, then implement your resolvers — thus implementing your API contract.

Designing the API contract first and then implementing it is often referred to as schema first API development and it is a good fit for GraphQL

Let’s get started!

Step 1 — Create your GraphQL Schema

Let’s say the root directory of your project is called my-awesome-project . Create your graphql schema file under my-awesome-project/src/cmd/go-graphql

For this blog post, I am going to use this To Do List schema from one of gqlgens example projects. It describes queries, mutations, an input type as well as directives.

Why create the src/cmd/go-graphql path? So that in the future, if we prefer, we could use the same project for non-graphql work. For example, we could have a Go REST API for To Do items under src/cmd/go-rest or a some front-end code under src/cmd/ui , leading to a directory layout as shown below.

my-awesome-project/

├── src/cmd

│ ├── go-graphql

| ├── <code for the GraphQL api>

│ ├── go-rest

| ├── <code for the REST api>

│ ├── ui

| ├── <code for front-end UI>

Step 2 — Install Go, docker and docker compose

Follow steps on the Go official download and install page

Follow steps on the docker official install page

Follow steps on the docker-compose official install page

Step 3 — Setup Git

Setup the git repository. This typically involves the following steps:

git init

git config user.name "some_username"

git config user.email "some_email_address"

You might want to create a .gitignore file under my-awesome-project/ as well, for obvious reasons.

Step 3 — Initialise your Go project

In Go, this is trivial to do. Let’s initialise our project and then add its first dependency within the go-graphql sub-folder:

cd my-awesome-project/src

go mod init gitlab.com/jigar_xyz/my-awesome-project

cd go mod init jigar.xyz/go-graphql/v2

cd cmd/go-graphql

go get github.com/99designs/gqlgen

go run github.com/99designs/gqlgen init --verbose

This will initialise a Go v2 module with the name gitlab.com/jigar_xyz/my-awesome-project . This module will contain a bunch of other auto-generated files in a well defined package layout.

If you run go run server.go at this point, a Go GraphQL API server will be spun up along with a GraphQL Playground at: http://localhost:8080. Point your browser to it and give it a go.

Note: This API complies with the default schema that the gqlgen init command generates under src/cmd/go-graphql/graph/schema.graphqls . It does not yet comply with our schema under src/cmd/go-graphql/schema.graphql .

Step 4 — Generate the GraphQL server for your schema!

First, we need to let gqlgen know where our GraphQL schema is. Modify the src/cmd/go-graphql/gqlgen.yml file and set schema to schema.graphql

schema:

- schema.graphql

At this point, we should delete the following obsolete files:

the default schema file src/cmd/go-graphql/graph/schema.graphqls

the default schema resolver file src/cmd/go-graphql/graph/schema.resolvers.go

Now run the gqlgen generate command to generate Go models, resolvers and a dummy server implementation for our target schema.

cd src/cmd/go-graphql

go run github.com/99designs/gqlgen generate --verbose

T he gqlgen.yml file gives you a lot of control over the code generation. More on that here

All the GraphQL boilerplate code for your spec is now ready. If you run go run server.go and point your browser to it, then inspect the GraphQL schema, you should see our target schema.

Initial GraphQL API Playground

Note: have a look at this commit to see what has changed under the hood. You can safely ignore the changes under the src/cmd/go-graphql/graph/generated folder — they are auto-generated files that do a lot of GraphQL specific heavy lifting, that we don’t need to worry about because of gqlgen

Step 5 — Setup implementation dependencies

gqlgen generates some dummy resolvers that simply panic with an error message not implemented . These are in src/cmd/go-graphql/graph/schema.resolvers.go

Before we go about implementing them, lets first think about how we will implement the functionality that the resolvers expose. For example, how will we save a given to do item in the database?

It’s good practice to keep your implementations separate from where you actually need them. This helps with code re-use, enables the use different implementations for a certain feature and also helps test each implementation easily. This follows the DRY principle of software development. It also makes your code a lot more readable.

With that in mind, let’s create an interface in src/pkg/todo/todo_service.go that clearly defines what we can do with a To Do item:

package todo



type ToDo interface {

Create(text string, isDone bool)

}

For now, all we can do is create to dos — we still don’t know how though.

Now, let’s create an implementation of this interface in src/pkg/postgres/todo_service.go — how do we do what the interface tells us to do?

package postgres



import "fmt"



type ToDoService struct {

Message string

}



func (t *ToDoService) Create(text string, isDone bool) {

fmt.Printf("I have a message: %s

", t.Message)

panic("oops!")

}

To implement an interface in Go, we just need to implement all the methods in the interface. Here, I’ve implemented the only method Create , from the ToDo interface.

I’ve also defined a ToDoService struct that holds the dependencies I need for this implementation to work. This is the method receiver for Create i.e. everything in the TodoService struct is available to us in the Create method. For now, lets just assume that I need a string , the implementation simply prints that string and then panics.

Now let’s make our resolver depend on this interface so that we can use all the functionality that the interface provides, without worrying about how its implemented.

The src/cmd/go-graphql/graph/resolver.go file has the Resolver struct that we can use to pull the ToDo interface in

package graph



import "gitlab.com/jigar_xyz/my-awesome-project/pkg/todo"



// This file will not be regenerated automatically.

//

// It serves as dependency injection for your app, add any dependencies you require here.



type Resolver struct {

ToDo todo.ToDo

}

Resolver is available to us in our actual resolver implementation in src/cmd/go-graphql/graph/schema.resolvers.go , thus giving us the ToDo interface to use in our resolver implementation.

We can implement on of our resolvers now, lets implement the CreateTodo resolver in src/cmd/go-graphql/graph/schema.resolvers.go :

func (r *myMutationResolver) CreateTodo(ctx context.Context, todo model.TodoInput) (*model.Todo, error) {

if todo.Done == nil {

r.ToDo.Create(todo.Text, true)

} else {

r.ToDo.Create(todo.Text, false)

}

panic(fmt.Errorf("not implemented"))

}

Now, we just hook in the implementation we want to use for the interface, when we create the Resolver in src/cmd/go-graphql/server.go . We could any implementation of the ToDo interface we want, but we’ll use the postgres implementation for now.

toDoService := &postgres.ToDoService{Message: "Hello!"}

srv := handler.NewDefaultServer(

generated.NewExecutableSchema(

generated.Config{Resolvers: &graph.Resolver{ToDo: toDoService}}))

Note: toDoService is a reference to the postgres implementation of the ToDo interface. Further, we just pass in Hello! as the string that the postgres implementation currently depends on

This is what our dependency graph looks like now

With this setup, we have:

successfully defined what we can do with To Do items in the ToDo interface

we can do with To Do items in the interface successfully defined how we can implement the ToDo interface with one implementation

we can implement the interface with one implementation ensured that our GraphQL resolvers can work with the ToDo interface so that they’re not coupled with the implementation — we can swap out the implementation without affecting the GraphQL related code in the resolvers (perhaps because we want to move to a different database or we want to test the resolvers with a mock database implementation)

interface so that they’re not coupled with the implementation — we can swap out the implementation without affecting the GraphQL related code in the resolvers (perhaps because we want to move to a different database or we want to test the resolvers with a mock database implementation) clearly defined dependencies for each layer, with dependency injection pushed all the way out, in the main server.go file

file the ability to re-use the ToDo interface and/or its implementations else where in the codebase, perhaps for exposing a REST API for To Do items under src/cmd/go-rest in the future

Have a look at this commit to review how we’ve set this all up.

Note that we have a few more packages and a corresponding repository layout as shown below

my-awesome-project/

├── src

│ ├── cmd

| ├── go-graphql

| ├── <code for the GraphQL api>

│ ├── pkg

| ├── todo

| ├── <ToDo Interface - no implementations>

| ├── postgres

| ├── <Postgres implementation of the ToDo Interface>

This resembles the repository layout explained in much more detail in this post about Go best practices, six years in

Now, lets create a docker compose definition file docker-compose.yml that we can use to quickly spin up a local postgres docker container for use in local development.

Now that we have an actual database, lets set up a database schema migration tool — this will allow us to create and maintain our database schema in code and make sure that whenever our application runs, it runs against the correct target schema. I’ll use the golang-migrate library in code to do this and I will place all our database schema creation and modification scripts under src/pkg/postgres/migrations right next to the postgres implementation. The schema creation scripts will include creation of the todo_item table, where we will store our to do items. I will also use the jackc/pgx postgres driver.

In the ToDo interface, I’ll add an Initialise() method so implementations can do whatever they want as part of their initialisation.

package todo



type ToDo interface {

Initialise() error

Create(text string, isDone bool)

}

This will allow our implementation to set up and maintain database the schema that it needs to run properly.

We implement the Initialise() method in the postgres implementation src/pkg/postgres/todo_service.go (using golang-migrate)

func (t *ToDoService) Initialise() error {

dbConnectionString := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", t.DbUserName, t.DbPassword, t.DbURL, t.DbName)

db, err := sql.Open("pgx", dbConnectionString)

if err != nil {

return err

}

driver, err := postgres.WithInstance(db, &postgres.Config{})

if err != nil {

return err

}

m, err := migrate.NewWithDatabaseInstance("file://../../pkg/postgres/migrations", t.DbName, driver)

if err != nil {

return err

}

err = m.Steps(2)

if err != nil {

return err

}

return nil

} toDoService := &postgres.ToDoService{

DbUserName: dbUserName,

DbPassword: dbPassword,

DbURL: dbURL,

DbName: dbName,

}



err := toDoService.Initialise()

if err != nil {

log.Fatal(err)

}



srv := handler.NewDefaultServer(

generated.NewExecutableSchema(

generated.Config{Resolvers: &graph.Resolver{ToDo: toDoService}}))

Then we make sure to call Initialise() as soon as we create an instance of the implementation, to use in src/cmd/go-graphql/server.go

toDoService := &postgres.ToDoService{

DbUserName: dbUserName,

DbPassword: dbPassword,

DbURL: dbURL,

DbName: dbName,

}



err := toDoService.Initialise()

if err != nil {

log.Fatal(err)

}



srv := handler.NewDefaultServer(

generated.NewExecutableSchema(

generated.Config{Resolvers: &graph.Resolver{ToDo: toDoService}}))

Now, we have:

a postgres database we can run locally

the database schema for our application

a database schema migration tool that will run whenever our application starts, set up the database with our schema (apply any updates to the schema, in case there are any updates to it) and ensure that our application only runs if the database schema requirements are met

If we run the following steps in order, we will have a self bootstrapped application that will run locally with the current implementation:

cd my-awesome-project

docker-compose up

cd src/cmd/go-graphql

POSTGRES_USER=graphql_api POSTGRES_PASSWORD=_please_change_me_now_ POSTGRES_URL=localhost POSTGRES_DB=graphql go run server.go

Note: the POSTGRES_* environment variables correspond to those in docker-compose.yml

Check out this commit to see how this is all set up.

Step 6 — Implement our GraphQL resolvers

Implementing all the resolver methods now is pretty trivial. Feel free to take a look at the various commits in the git repository. Here are some notable changes:

here we implement our first resolver — the create to do item resolver. Note the changes in our database schema and in the Initialise() method of the postgres implementation — where we check the schema version and run database migrations explicitly by setting a targetSchemaVersion

method of the postgres implementation — where we check the schema version and run database migrations by setting a here we change a mutation in our GraphQL schema updateTodo(id: ID!, changes: Map!): Todo to updateTodo(id: ID!, updatedTodo: TodoInput!): Todo! because we want the full ToDoInput when updating a to do item, instead of relying on just a Map . Because our GraphQL schema changed, I ran go run github.com/99designs/gqlgen generate --verbose again to reflect the change in generated code

to because we want the full when updating a to do item, instead of relying on just a . Because our GraphQL schema changed, I ran again to reflect the change in generated code here we added a struct next to the ToDo interface definition, because we want to use it to represent a ToDo item we get from the database. Our implementations will need to return this struct which will be used wherever the Get(id string) (*ToDoItem, error) method is used. It is good practice to keep all structs used by an interface in the same file as the interface itself —everything to do with the interface is all in one place

Now that we’ve implemented our resolvers, we’re ready to run the API locally and play around with it a little bit.

cd my-awesome-project

docker-compose up

cd src/cmd/go-graphql

POSTGRES_USER=graphql_api POSTGRES_PASSWORD=_please_change_me_now_ POSTGRES_URL=localhost POSTGRES_DB=graphql go run server.go

The console log prints



2020/10/09 11:20:18 Migrating database schema from version: -1 to version 2

2020/10/09 11:20:18 connect to 2020/10/09 11:20:18 Target database schema version is: 2 and current database schema version is: -12020/10/09 11:20:18 Migrating database schema from version: -1 to version 22020/10/09 11:20:18 connect to http://localhost:8080/ for GraphQL playground

Great, our database is up and running with everything set up. Our resolvers should work as expected now.

Create a To Do

Get To Do items

todo_item table in the database

Step 7 — Implement our directives

Our GraphQL schema has a couple of directives we still need to implement

directive @hasRole(role: Role!) on FIELD_DEFINITION

directive @user(id: ID!) on MUTATION | QUERY | FIELD

The field definition for the done field on the ToDo type in our schema is associated with the @hasRole directive

type Todo {

id: ID!

text: String!

done: Boolean! @hasRole(role: OWNER) # only the owner can see if a todo is done

}

When we try to access this field without implementing the directive, we get an error as expected

Missing directive implementation

Implementing directives is pretty straight forward with gqlgen as explained here. Let’s implement a directive in src/cmd/go-graphql/server.go that simply prints a log message for now

func hasRoleDirective(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error) {

log.Printf("Inside hasRoleDirective - ignore the role check for now")

return next(ctx)

}

Now, we can use this directive with our server

config := generated.Config{Resolvers: &graph.Resolver{ToDo: toDoService}} config.Directives.HasRole = hasRoleDirective // referencing the method above



srv := handler.NewDefaultServer(generated.NewExecutableSchema(config))

When we run the query again after restarting the server, we see the done field as expected

Directive implemented, output as expected

And the server logs indicate that our directive did indeed execute



2020/10/09 12:01:07 No database schema migrations need to be performed.

2020/10/09 12:01:07 connect to

2020/10/09 12:01:09 Inside hasRoleDirective - ignore the role check for now 2020/10/09 12:01:07 Target database schema version is: 2 and current database schema version is: 22020/10/09 12:01:07 No database schema migrations need to be performed.2020/10/09 12:01:07 connect to http://localhost:8080/ for GraphQL playground2020/10/09 12:01:09 Inside hasRoleDirective - ignore the role check for now

Check this commit to see the actual code changes.

And there you have it, the To Do list GraphQL API is up and running locally!

Next Steps

I encourage you to check-out/fork the git repository for this project and play around with it. Try changing the GraphQL schema and then implementing those changes in code. Or try modifying the ToDo interface and see what breaks.

And like I mentioned earlier, in future posts, I will highlight how one can use Gitlab to build, test and deploy this API.