How To use GraphQL with Go
How to build a GraphQL API with Go and Postgres using glqgen and gorm.
- 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
- Have Golang version 1.1x installed on your machine, if you do not have this installed on your machine, you can follow this guide.
- Knowledge of Golang.
- Knowledge of REST APIS
- 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:
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 theCRUD
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 thegorm 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 theBookService
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