Build a Graphql API with gqlgen and MySQL

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.com/99designs/gqlgen

We then generate our project skeleton

go run github.com/99designs/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.com/99designs/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.Book
  Author:
    model: github.com/AlexSwiss/bookworm/graph/models.Author

Now delete schema.resolver.go file and let’s regenerate our code by running

go run github.com/99designs/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.com/go-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!

Learn Something New Everyday,
Connect With The Best Developers!

Sign Up Now!

& 500k+ others use Hashnode actively.

No Comments Yet