How To use GraphQL with Go

How to build a GraphQL API with Go and Postgres using glqgen and gorm.

·

9 min read

How To use GraphQL with Go
  • Introduction to what we are building
  • Prerequisite
  • What is graphql
  • Setup the project
  • Install required packages
  • setup the database
  • setup the env file
  • setup the glqen gen
  • reset schema file
  • generate from the resolver file
  • write functions to get info/slap info from the DB
  • attach the functions to the mutation resolver
  • setup the main file
  • start and test
  • conclusion

Introduction

This guide explains how to build a web API in Go with graphql using the gqlgen and connect it to a Postgres database with gorm. At the end of this article, you should be able to use graphql to write basic CRUD and understand the basics of query and mutation in graphql.

Prerequisites

  1. Have Golang version 1.1x installed on your machine, if you do not have this installed on your machine, you can follow this guide.
  2. Knowledge of Golang.
  3. Knowledge of REST APIS
  4. Have Postgres installed on your machine, if you do not have this installed on your machine, you can follow this guide.

What is Graphql

Graphql is an open-source query language used to query and manipulate data from the server-side, this was developed internally by Facebook before being moved to the Graphql foundation.

Setup The Project

  • In your terminal, create a go-graphql directory and enter the directory.
$ mkdir go-graphql
$ cd go-graphql
  • Initialize the project.

    go mod init github.com/<your GitHub username>/go-graphql

Install the Required Packages

  • Install Gorm- this will be used for the Object Relational Mapping
$ go get -u gorm.io/gorm
  • Install Gorm Postgres driver - this will be used for communicating with the Postgres database
$  go get -u gorm.io/driver/postgres
  • Install Godotenv - this will be used to load environmental variables to our code
$ go get github.com/joho/godotenv
  • Install Glqen to your go tools - Go library for graphql
$ printf '// +build tools\npackage tools\nimport _ "github.com/99designs/gqlgen"' | gofmt > tools.go
$ go mod tidy
$ go run github.com/99designs/gqlgen
  • Install the glqgen handler if it does not get installed with it automatically:

Setup the database

  • Enter the Postgres user interactive shell to execute the Postgresql commands
sudo -iu postgres psql
  • Create the database.
CREATE DATABASE gographql;
  • Exit the Postgres interactive shell.
\q
`

Setup the ENV file

Create a .env file that will hold your environmental variables and add the following lines of code to it:

DB_HOST=localhost
DB_PORT=5432
DB_USER=yourusername
DB_PASS=yourpassword
DB_NAME=gographql
DB_SSLMODE=disable

Setup the glqgen package

Run the command below to generate the necessary files needed for the graphql integration:

go run github.com/99designs/gqlgen init

The folder structure is meant to look like this after the command generates the folders:

image.png

Setting up our Schema file

In your graph/schema.graphqls file you will find a to-do and user mutation, input, and query created for you. For this step, you will delete the file content and replace it with the book query, input, and mutation below.

type Book {
  id: Int!
  title: String!
  author: String!
  publisher: String!
}

input BookInput{
  title: String!
  author: String!
  publisher: String!
}
type Mutation{
  CreateBook(input: BookInput!): Book!
  DeleteBook(id: Int!): String!
  UpdateBook(id: Int!): String!

}
type Query{
  GetAllBooks: [Book!]!
  GetOneBook(id: Int!): Book! 
}

After changing the content of the schema, what is needed next is running the command to regenerate the whole folder to implement the schema changes. Here is the command: go run github.com/99designs/gqlgen

Setting up our database connection

Create a folder called app in the app folder create a database folder and in the database folder, create a file called postgres.go, paste the following code in the postgres.go file

package database

import (
    "fmt"

    "github.com/iyiola-dev/go-graphql/graph/model"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type Config struct {
    Host     string
    Port     string
    Password string
    User     string
    DBName   string
    SSLMode  string
}

func NewConnection(config *Config) (*gorm.DB, error) {
    dsn := fmt.Sprintf(
        "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
        config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode,
    )
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return db, err
    }
    return db, nil
}

The NewConnection function creates the database connection and returns the database connection if there is no error.

Creating the Migration Model

For Migrating the model, You have to create a model outside the generated model which will allow the adding of gorm tags as the generated model should not be edited. Create a folder in the app folder and name it models, create a file under the models folder, and name it book_model.go. Insert the following lines of code in the book_model.go:

package models

type Book struct {
    ID        int    `gorm:"primary key;autoIncrement" json:"id"`
    Title     string `json:"title"`
    Author    string `json:"author"`
    Publisher string `json:"publisher"`
}

Right after this, go to the app/database/postgres.go file and add the Migrate function under the NewConnection function. The Migrate function:

func Migrate(db *gorm.DB) error {
    return db.AutoMigrate(&models.Book{})
}

Creating our Repository and CRUD functionalities

For the repository functions, you will create a repository folder and in the folder, you will create a book_repo.go file, this is where all the code for the book CRUD functionality will be. Four things will be in this file:

  • The BookRepository Interface which will hold the CRUD functions as shown below:
type BookRepository interface {
    CreateBook(bookInput *model.BookInput) (*models.Book, error)
    UpdateBook(bookInput *model.BookInput, id int) error
    DeleteBook(id int) error
    GetOneBook(id int) (*models.Book, error)
    GetAllBooks() ([]*model.Book, error)
}
  • The BookService struct which will hold an instance to the gorm DB as shown below:
type BookService struct {
    Db *gorm.DB
}

var _ BookRepository = &BookService{}
  • The NewBookService function which takes an argument of the gorm DB instance and returns a pointer to the BookService struct as shown below:
func NewBookService(db *gorm.DB) *BookService {
    return &BookService{
        Db: db,
    }
}
  • The CRUD functions consist of the create, update, delete and get functions.

The first one will be the create function which takes an argument of a pointer to the bookInput model and returns a pointer to the book model and an error, this is how it will be implemented:

func (b *BookService) CreateBook(bookInput *model.BookInput) (*models.Book, error) {
    book := &models.Book{
        Title:     bookInput.Title,
        Author:    bookInput.Author,
        Publisher: bookInput.Publisher,
    }
    err := b.Db.Create(&book).Error

    return book, err
}

The second one will be the update function which takes an argument of the id of the book to be updated and a pointer to the bookInput model, this is how it will be implemented below:

func (b *BookService) UpdateBook(bookInput *model.BookInput, id int) error {
    book := models.Book{
        ID:        id,
        Title:     bookInput.Title,
        Author:    bookInput.Author,
        Publisher: bookInput.Publisher,
    }
    err := b.Db.Model(&book).Where("id = ?", id).Updates(book).Error
    return err
}

The third one will be the delete function which takes an argument of the id of the book to be deleted and it returns an error, this is how it will be implemented below:

func (b *BookService) DeleteBook(id int) error {
    book := &models.Book{}
    err := b.Db.Delete(book, id).Error
    return err
}

The fourth one will be the GetOneBook function which takes the id of the book to be gotten and returns a pointer to the books model and an error, this is how it will be implemented:

func (b *BookService) GetOneBook(id int) (*models.Book, error) {
    book := &models.Book{}
    err := b.Db.Where("id = ?", id).First(book).Error
    return book, err
}

The last one will be the GetAllBooks function which does not take any argument and it returns all the books present in the database, this is how it will be implemented:


func (b *BookService) GetAllBooks() ([]*model.Book, error) {
    books := []*model.Book{}
    err := b.Db.Find(&books).Error
    return books, err

}

Adding the dependencies to the Resolver

In the graph folder there is a file called resolver.go if you check it you will see a struct named Resolver the function is to hold all the app dependencies used in the development, in this case, there is only one dependency which is the book repository, it will be added like this:

type Resolver struct {
    BookRepository repository.BookRepository
}

Adding the repository functions to the schema resolver.

In the graph folder, there is a file called schema.resolver.go, present in this file are the functions generated by the schema, each for each CRUD operation w performed in the repository file, they will be added below like this:

Create


func (r *mutationResolver) CreateBook(ctx context.Context, input model.BookInput) (*model.Book, error) {
    book, err := r.BookRepository.CreateBook(&input)
    bookCreated := &model.Book{
        Author:    book.Author,
        Publisher: book.Publisher,
        Title:     book.Title,
        ID:        book.ID,
    }
    if err != nil {
        return nil, err
    }
    return bookCreated, nil
}

Update

func (r *mutationResolver) UpdateBook(ctx context.Context, id int, input model.BookInput) (string, error) {
    err := r.BookRepository.UpdateBook(&input, id)
    if err != nil {
        return "nil", err
    }
    successMessage := "successfully updated"

    return successMessage, nil
}

Delete

func (r *mutationResolver) DeleteBook(ctx context.Context, id int) (string, error) {
    err := r.BookRepository.DeleteBook(id)
    if err != nil {
        return "", err
    }
    successMessage := "successfully deleted"
    return successMessage, nil
}

GetOneBook


func (r *queryResolver) GetOneBook(ctx context.Context, id int) (*model.Book, error) {
    book, err := r.BookRepository.GetOneBook(id)
    selectedBook := &model.Book{
        ID:        book.ID,
        Author:    book.Author,
        Publisher: book.Publisher,
        Title:     book.Title,
    }
    if err != nil {
        return nil, err
    }
    return selectedBook, nil
}

GetAllBooks

func (r *queryResolver) GetAllBooks(ctx context.Context) ([]*model.Book, error) {
    books, err := r.BookRepository.GetAllBooks()
    if err != nil {
        return nil, err
    }
    return books, nil
}

Creating the main function

In the server.go file there are functions already generated, so what will be done in this article is instantiating the database, running migrations, initiating the repo and adding the repo to the resolver struct as shown below:

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/playground"
    "github.com/iyiola-dev/go-graphql/app/database"
    "github.com/iyiola-dev/go-graphql/app/repository"
    "github.com/iyiola-dev/go-graphql/graph"
    "github.com/iyiola-dev/go-graphql/graph/generated"
    "github.com/joho/godotenv"
)

const defaultPort = "8080"

func main() {
    godotenv.Load()
    config := &database.Config{
        Host:     os.Getenv("DB_HOST"),
        Port:     os.Getenv("DB_PORT"),
        Password: os.Getenv("DB_PASS"),
        User:     os.Getenv("DB_USER"),
        SSLMode:  os.Getenv("DB_SSLMODE"),
        DBName:   os.Getenv("DB_NAME"),
    }
    db, err := database.NewConnection(config)
    if err != nil {
        panic(err)
    }
    database.Migrate(db)
    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }
    repo := repository.NewBookService(db)
    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{
        BookRepository: repo,
    }}))

    http.Handle("/", playground.Handler("GraphQL playground", "/query"))
    http.Handle("/query", srv)

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

if you run the command go run server.go this should start the database and migrate the model, after this, you can run http://localhost:8080/ in your browser you should see a playground where you can run your queries and mutations

Conclusion

You should be able to freely use the glqgen package to do a basic CRUD API with go connecting to the database and using the gorm package with it, link to the repo: repo