In this article, we will be building a graphql API for a book club where members can perform basic CRUD operations. Our backend language will be gqlgen a Go library for building graphql without stress. As oppose to REST, graphql possesses good advantage when we talk about optimizing the web and as such combining it with Golang increases productivity as gqlgen is schema first and generates our skeletal code which allows us to focus on the logic and not the configuration.
PRE-REQUISITES
This tutorial assumes you already have basic Go knowledge and you are familiar with how a graphql query looks like. We will also be using MySQL for our database so ensure you have downloaded MySQL workbench or if you use Xampp; start you apache server and make sure its running.
INITIAL SETUP
Create a folder for our project; follow instructions here
$ mkdir bookworm
$ cd bookworm
$ go mod init github.com/[username] /bookworm
$ go get github.com99designs/gqlgen
We then generate our project skeleton
go run github.com99designs/gqlgen init
when we open our project folder we will see the structure generate for us by gqlgen. It will look like this
go.mod
├── go.sum
├── gqlgen.yml - The gqlgen config file, knobs for controlling the generated code.
├── graph
│ ├── generated - A package that only contains the generated runtime
│ │ └── generated.go
│ ├── model - A package for all your graph models, generated or otherwise
│ │ └── models_gen.go
│ ├── resolver.go - The root graph resolver type. This file wont get regenerated
│ ├── schema.graphqls - Some schema. You can split the schema into as many graphql files as you like
│ └── schema.resolvers.go - the resolver implementation for schema.graphql
└── server.go - The entry point to your app. Customize it however you see fit
Defining our Graphql Shema
Since gqlgen is a schema first library we define our schema first and it helps us generate our model based on the schema we have defined. We will define our schema Inside of our graph directory. Navigate to the graph directory and delete the schema.graphqls file, then create a new file “schema.graphql”.
schema.graphql
type Book {
id: Int!
name: String!
category: String!
author: [Author!]!
}
type Author {
id: Int!
firstname: String!
lastname: String!
bookID: ID!
}
type Query {
books(search: String=""): [Book!]!
}
input newBook {
name: String!
category: String!
}
input newAuthor {
firstname: String!
lastname: String!
}
type Mutation {
addBook(input: newBook, author: [newAuthor]): Book!
editBook(id: Int, input: newBook, author: [newAuthor] = []): Book!
deleteBook(id: Int): [Book!]!
}
In our graph directory we can find our gqlgen.yml file which we will tweak to point to our new schema file. In the yml file change the schema.graphls to schema.graphl
Now run
go run github.com99designs/gqlgen generate
We can see that our models have been regenerated to match with our schema. Awesome right? To define our own fields we need to create our custom model. Create a new folder “models” and create a file “models.go” then add this
models/models.go
package models
type Author struct {
ID int `json:"id" gorm:"primary_key"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
BookID int `json:"bookID"`
}
type Book struct {
ID int `json:"id" gorm:"primary_key"`
Name string `json:"name"`
Category string `json:"category"`
Author []*Author `json:"author"`
}
The gorm primary key tags helps us with primary key during database migrations. Now add our new models to our .yml file mapping. Add this below the model key
models:
Book:
model: github.com/AlexSwiss/bookworm/graph/models…
Author:
model: github.com/AlexSwiss/bookworm/graph/models…
Now delete schema.resolver.go file and let’s regenerate our code by running
go run github.com99designs/gqlgen
Resolver.go This is the file where we write our CRUD logic. Resolvers basically contain the logic of our objects, queries, and mutations in graphql. Our resolvers have been generated for us with gqlgen. The whole file will look like this
package graph
// 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.
import (
"context"
"github.com/AlexSwiss/bookworm/graph/generated"
"github.com/AlexSwiss/bookworm/graph/model"
"github.com/AlexSwiss/bookworm/graph/models"
)
func (r *mutationResolver) AddBook(ctx context.Context, input *model.NewBook, author []*model.NewAuthor) (*models.Book, error) {
}
func (r *mutationResolver) EditBook(ctx context.Context, id *int, input *model.NewBook, author []*model.NewAuthor) (*models.Book, error) {
}
func (r *mutationResolver) DeleteBook(ctx context.Context, id *int) ([]*models.Book, error) {
}
func (r *queryResolver) Books(ctx context.Context, search *string) ([]*models.Book, error) {
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Database connection
First we install our modules and packages that we will need.
go get -u github.com/jinzhu/gorm
go get -u github.comgo-sql-driver/mysql
The first package “gorm” is an ORM that will perform SQL queries on our database and “go-sql-driver” is a module that helps us connect Go to our SQL database.
Lets write our function that helps us connect our database. Add this after our import at top of the file
models/models.go
func FetchConnection() *gorm.DB {
db, err := gorm.Open("mysql", "root:rosecransB430@/bookworm")
if err != nil {
panic(err)
}
return db
}
Next we write our function for migration of DB. Add this inside our function, at the top
server.go
//Migrate Db
db := models.FetchConnection()
db.AutoMigrate(&models.Book{}, &models.Author{})
db.Close()
Logic Mutation Inside our resolver file, let’s write our query that creates (add) our data to the database
schema.resolver.go
package graph
// 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.
import (
"context"
"github.com/AlexSwiss/bookworm/graph/generated"
"github.com/AlexSwiss/bookworm/graph/model"
"github.com/AlexSwiss/bookworm/graph/models"
)
func (r *mutationResolver) AddBook(ctx context.Context, input *model.NewBook, author []*model.NewAuthor) (*models.Book, error) {
db := models.FetchConnection()
defer db.Close()
//create book using input struct
book := models.Book{
Name: input.Name,
Category: input.Category,
}
book.Author = make([]*models.Author, len(author))
for index, item := range author {
book.Author[index] = &models.Author{Firstname: item.Firstname, Lastname: item.Lastname}
}
db.Create(&book)
return &book, nil
}
func (r *mutationResolver) EditBook(ctx context.Context, id *int, input *model.NewBook, author []*model.NewAuthor) (*models.Book, error) {
}
func (r *mutationResolver) DeleteBook(ctx context.Context, id *int) ([]*models.Book, error) {
}
func (r *queryResolver) Books(ctx context.Context, search *string) ([]*models.Book, error) {
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Now if you run our server.go file, we should be able to connect to our playground on Localhost:8080. You can run the query below;
mutation addBook {
addBook(
input: { name: "Americana", category: "First stew it" }
author: [{ firstname: "Chimamanda" }, { lastname: "Adichie" }]
) {
id
name
category
author {
firstname
lastname
}
}
}
Awesome!!!!!!!!! Now we can add the rest of our logic; query(read), update, delete like this
package graph
// 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.
import (
"context"
"github.com/AlexSwiss/bookworm/graph/generated"
"github.com/AlexSwiss/bookworm/graph/model"
"github.com/AlexSwiss/bookworm/graph/models"
)
func (r *mutationResolver) AddBook(ctx context.Context, input *model.NewBook, author []*model.NewAuthor) (*models.Book, error) {
db := models.FetchConnection()
defer db.Close()
//create book using input struct
book := models.Book{
Name: input.Name,
Category: input.Category,
}
book.Author = make([]*models.Author, len(author))
for index, item := range author {
book.Author[index] = &models.Author{Firstname: item.Firstname, Lastname: item.Lastname}
}
db.Create(&book)
return &book, nil
}
func (r *mutationResolver) EditBook(ctx context.Context, id *int, input *model.NewBook, author []*model.NewAuthor) (*models.Book, error) {
db := models.FetchConnection()
defer db.Close()
var book models.Book
//find book based on ID
db = db.Preload("Authors").Where("id = ?", *id).First(&book).Update("name", input.Name)
if input.Category != "" {
db.Update("category", *&input.Category)
}
//update author
book.Author = make([]*models.Author, len(author))
for index, item := range author {
book.Author[index] = &models.Author{Firstname: item.Firstname, Lastname: item.Lastname}
}
db.Save(&book)
return &book, nil
}
func (r *mutationResolver) DeleteBook(ctx context.Context, id *int) ([]*models.Book, error) {
db := models.FetchConnection()
defer db.Close()
var book models.Book
//fetch based on ID and delete
db.Where("id = ?", *id).First(&book).Delete(&book)
//preload and fetch all recipe
var books []*models.Book
db.Preload("Author").Find(&books)
return books, nil
}
func (r *queryResolver) Books(ctx context.Context, search *string) ([]*models.Book, error) {
db := models.FetchConnection()
defer db.Close()
var books []*models.Book
//preload loads the author relationship into each book
db.Preload("Author").Find(&books)
return books, nil
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
If we restart the server we would be able to perform various CRUD operations.
Conclusion
we were able to build a simple GraphQL API with MySQL using GORM as the ORM framework with the help of gqlgen library. I hope you found this post useful and falling inlove with Go already. You can checkout the complete code for this API from Github . You can go ahead and implement auth, pagination etc. Please leave a comment below if you have any feedback. Follow me on twitter on @CodedFingers Cheers!