SvelteKit
GitHub
Back

Build a simple GraphQL API with Go

Introduction

The technical definition of GraphQL is: an open-source data manipulation and query language for APIs. It's also a runtime used for fulfilling graph queries with existing data.  To simplify, GraphQL is both a query language, as well as a runtime environment that allows you to fulfill queries using the GraphQL syntax, while also creating shared-schemas between different services and applications.

In this module I hope to bridge the gap between transitioning from using a traditional REST-API to using GraphQL to provide data for our applications. This article itself acts as both an information resource and a training document for referencing.

Prerequisites

  1. Go installation - Preferably >= 1.19
  2. A basic knowledge and understanding of Go
  3. A basic understanding of GraphQL, you can find in-depth tutorials on GraphQL here

Setting up the project

Typically in the setup sections I would briefly go over creating an initial project directory. However, because we are using the gqlgen library to bootstrap our GraphQL server + schema there are a few extra steps I will document here.

First we'll start off by installing the primary gqlgen tool:

$ go install github.com/99designs/gqlgen@latest

Now we can create and initialize our typical Go project

$ mkdir gqlexample
$ cd gqlexample
$ go mod init gqlexample

Our project directory is now set up. We'll need to create a tools.go file to store our build tools

// +build tools

package tools

import _  "github.com/99designs/gqlgen"

Execute the tidy command to install dependencies

$ go mod tidy

Lastly for this section we will use the installed gqlgen tool to generate all the boilerplate files that are necessary for the GraphQL API

$ gqlgen init

The gqlgen command generates a server.go file that runs the actual GraphQL runtime server, as well as a /graph directory containing a schema.graphqls file which holds the schema definitions for our GraphQL API. In this project we won't necessarily have to interact with the server file. We will however be using the schema.graphqls file to define the models that we will be interacting with using GraphQL!

GraphQL Schema Definition

In the first section we set up our project directory and using the gqlgen tool to generate some boilerplate files. In this section we'll be interacting with these files to define our schema. If you're new or only slightly familiar with GraphQL the next few steps may seem confusing, but I'll try to do my best to be esoteric with explaining.

To define our own schema we're going to open up the schema.graphqls file and drop in our own code. Considering this is module is meant to be an introduction to GraphQL we'll keep it simple with only creating a User model and functionality for creating/querying the user.

Open the schema.graphqls file and replace the code with the code below:

//schema.graphqls

scalar Upload

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

type Query {
    users: [User]!
}

input NewUser {
    name: String!
    email: String!
    createdAt: String
}

type Mutation {
    createUser(input: NewUser!): User!
}

OK, what did we do?

Gqlgen automatically defines the Upload scalar type, and the properties of a file. It's defined once at the top of the file.

We defined a User type which is the core data model of what our user will look like in our database and what our application will receive when querying for a full user.

Also defined is a Query. The query specifies when querying for users the server will expect the User type to be returned.

Our input is defined as NewUser meaning when creating a new user these are the fields expected to be present in the request body.

Finally we declared a Mutation. If you're not familiar with Mutations that's OK, the definition is in the name. It's meant to store functions that mutate or change the state of our database. In this case we defined a single function in our mutation called createUser, and you guessed it.. it creates a user. The createUser mutation accepts our NewUser input and returns a full User model!

Generating Resolvers

In the previous step we modified our graph schema to define a few data models, a query, and a mutation for creating users. That's fantastic, but we still need a way for the outside world to interact with this functionality. Lucky for us gqlgen has thought about that and includes a tool to generate the resolvers based on our defined schema!

Run the following command to generate the resolvers:

$ gqlgen generate

Don't be afraid, but this command will probably do a few things. You may receive some validation errors, and you probably have a few new folders + files in your project. Don't worry if this looks complex, once you take a peek into each file the context is fairly idiomatic.

To resolve the validation errors simply delete the extraneous mutationResolvers gqlgen moved to the bottom of schema.resolvers.go these were sample functions and are no longer need ( they're also not modeled in our schema ) so we can easily remove them. While in this file also notice that we have functions related to our schema that have been generated!

  • CreateUser <- This is used to create a new user utilizing our User model
  • User <- Used to request/query our User model

    I'll keep this section short because we did exactly what we intended on doing. We generated resolvers for our schema!

Database Interactions

For the sake of the length of this already arduously long module I will not go into depth about how to set up a database connection in Go. I will cover this in another shorter module. However, In this section I will describe how to use a database connection in tandem with GraphQL.

It's recommended to create a db.go file under the graph directory. In this file we can specify a function that will automatically create the schema in our database!

package graph

import (
    //... Your database library
)

func createSchema(db *pg.DB) error {
    for _, models := range []interface{}{(*model.User)(nil)}{
        if err := db.Model(models).CreateTable(&orm.CreateTableOptions{
            IfNotExists: true,
        }); err != nil {
            panic(err)
        }
    }
    return nil
}

This function will create the tables in our database for us. Using IfNotExists is helpful in only creating the table if it doesn't already exist!

Below the createSchema function you should create a Connect function to actually connect to your database.

Now we're going to move into the server.go file to initialize our database and start GraphQL!

In this file we're going to replace the existing srv variable with our database handler:

package main

import (
    ...
)

const defaultPort = "8080"

func main() {
    // ...

    Database := graph.Connect()
    srv := handler.NewDefaultServer(
        generated.NewExecutableSchema(
            generated.Config{
                Resolvers: &graph.Resolver{
                    DB: Database,
                },
            }),
        )

    // ...
}

Here we're essentially bootstrapping the database connection using the Connect function and starting call for the Resolver struct. The only issue is our Resolver struct doesn't contain a database connection. But we can quickly fix that by adding our db connection to the Resolver struct in resolver.go:

package graph

import "your-database-driver"

type Resolver struct {
    DB *a.Db
}

Now your app has a database connection that can be established when the server.go is invoked and your app is started. At this point personally I'm using go-pg which is dual used as an ORM so it's injected as well.

This section might have seemed a bit complicated, if not, awesome! Thankfully we're just about out of the weeds. In the last and final section we'll go over finishing the implementation of our resolvers that we left blank before. Then we can start querying!

Implementing Resolvers

If you're familiar with building traditional REST-APIs in Go then you shouldn't have any problems with the following section. We're going to be filling out the functionality of the CreateUser and Users functions that were generated for us earlier.

Let's jump back into schema.resolvers.go and write some code!

import (
    //...
    "github.com/google/uuid"
)

func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
    user := model.User{
        ID:        fmt.Sprintf("%v", uuid.New()),
        Name:      input.Name,
        Email:     input.Email,
        CreatedAt: time.Now().Format("01-02-2003"),
    }

    _, err := r.DB.Model(&user).Insert(); if err != nil {
        return nil, fmt.Errorf("error inserting user: %v", err)
    }

    return &user, nil
}

In the mutation above we defined a few items. First, we accept our NewUser model as input and add a UUID ( I'm using the google/uuid package ). I also added a timestamp for our created time. After that I'm using the ORM to insert the new model into the database. Finally we handle any error from the response, if there are none we return the created user!

We can now run our app to test creating users. Start the app with:

$ go run ./server.go

If you navigate to the defined port on our local system you can access a build in GraphQL editor ( localhost:8080 ). You can paste the create mutation below to create a new user:

mutation createUser{
    createUser(
        input: {
            email: "[email protected]"
            name: "Bilbo Baggins"
        }
    ) {
      id
    }
}

If you execute this mutation you  should receive a response that contains a data key returning the id of your newly created user!

For the last step in this model we will implement the Users query resolver to be able to query our users from the database.

Head back to the schema.resolvers.go file and add the following code under the Users function:

func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
    var users []*model.User

    err := r.DB.Model(&users).Select()
    if err != nil {
        return nil, err
    }

    return users, nil
}

Theoretically you could expand the Select method to include clauses like LIMIT or WHERE to narrow the results. For this example we're just returning all users.

Head back to the browser and in our GraphQL sandbox enter the following query:

query fetchUsers{
    users {
        name
        id
        email
    }
}

Your output should match the single user we created in the earlier step!

Summary

In this module we used the gqlgen library and created a Go project that defines a graph schema and mutations that we can use to interact with our GraphQL server using graph queries.

In future articles I'll go over more deployment methods like containerizing and deploying in a multitude of different ways.