Golang and graphql: Building a recipe CRUD api with golang, graphql and mysql

Golang and graphql: Building a recipe CRUD api with golang, graphql and mysql

Add to bookmarks

Sat May 25 2019

Graphql has been one of the best things to happen to web development yet. It allows you to query data from an api in a structured and "safe" manner using its query language. In this walkthrough, we will be creating a GraphQl API with golang that allows us to create food recipes (with ingredients), read them, update and delete them from a MySQL database.

Prerequisites

Setting up

Begin by creating the folder that houses your project outside the $GOPATH so we can use modules, we can call it gorecipe. In it, run

go mod init github.com/<username>/gorecipe

This initializes your project as a module. Now you can install gorm, which we will use as our ORM (well, almost). Install it with the command, should take a while:

go get -u github.com/jinzhu/gorm

Once that's done, you need to make sure you have a working MySQL server with a database for the tutorial, you can call it golang. Once that's done, let's move on to the code.

Code

For graphql, we will be using gqlgen. It happens to be one of the easiest to use graphql server libraries to use (in my personal opinion).

The first thing to do is to create a schema.graphql file at the root of the folder. This file would describe our Api using the schema definition library, so go ahead and add this to the file:

type Recipe{
    id: Int!
    name: String!
    procedure: String!
    ingredients: [Ingredient!]!
}

type Ingredient{
    id: Int!
    name: String!
    recipeId: Int!
}

type Query{
    recipes(search: String = ""): [Recipe!]!
}

input NewRecipe{
    name: String!
    procedure: String
}

input NewIngredient{
    name: String!
}

type Mutation{
    createRecipe(input: NewRecipe, ingredients: [NewIngredient]): Recipe!
    updateRecipe(id: Int, input: NewRecipe, ingredients: [NewIngredient] = []): Recipe!
    deleteRecipe(id: Int): [Recipe!]!
}

From the contents you can see that we define a few objects, queries and mutations:

  • The recipe and ingredient object represents a single recipe and ingredient respectively and their available data
  • The recipes query will be for fetching all recipes and would return an array of Recipes back.
  • Then we define a few mutations with their arguments for creating, updating and deleting a recipe respectively

The next thing to do is to let gqlgen generate the server code for us, run this at the root of your project

$ go run github.com/99designs/gqlgen init

Now once it's done your project structure should look like this:

server/server.go //Run this to start the server
generated.go //Generated resolver source codes
go.mod //Generate mod file
go.sum // Generated mod file
gqlgen.yml //Yml file for mapping models and resolvers
models_gen.go // Holds model to graphql types
resolver.go // contains the resolvers to be used, can be edited
schema.graphql //Schema file

If you open the models_gen.go file that contains our models you should see this

// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.

package gorecipe

type Ingredient struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    RecipeID int    `json:"recipeId"`
}

type NewIngredient struct {
    Name string `json:"name"`
}

type NewRecipe struct {
    Name      string  `json:"name"`
    Procedure *string `json:"procedure"`
}

type Recipe struct {
    ID          int           `json:"id"`
    Name        string        `json:"name"`
    Procedure   string        `json:"procedure"`
    Ingredients []*Ingredient `json:"ingredients"`
}

As you can see this contains all the models we would be using in our app, but in our case we will need to work with our own model definition. You can create the file models/models.go and this to it.

package models

type Ingredient struct {
    ID       int    `json:"id" gorm:"primary_key"`
    Name     string `json:"name"`
    //Foreign key
    RecipeID int    `json:"recipeId"`
}

type Recipe struct {
    ID          int           `json:"id" gorm:"primary_key"`
    Name        string        `json:"name"`
    Procedure   string        `json:"procedure"`
    //Ingredients owned by a recipe
    Ingredients []Ingredient `json:"ingredients"`
}

From this new model, the only thing of importance that was added is the gorm primary_key tags. This would help us with the primary key during migrations.

Now to add them to our YML file mapping. Open the gqlgen.yml and add this right below the model key

models:
  Recipe:
    model: github.com/kofoworola/gorecipe/models.Recipe
  Ingredient:
    model: github.com/kofoworola/gorecipe/models.Ingredient

What we are doing is essentially pointing our Recipe and Ingredient objects to their respective models when "encoding and decoding" responses.

Now regenerate the generated code by running

go run github.com/99designs/gqlgen

Resolvers

Resolvers basically contain the logic of our objects, queries, and mutations in graphql. With Gqlgen in this case, the resolver.go file contains structs that act as our resolvers, specifically MutationResolver and QueryResolver. You would be pleased to know that other resolvers for the objects (Recipe and Ingredient) have been generated and managed in the generated.go file.

The only time you would need to edit the resolver for objects is if our models (structs that define objects) contain different attributes. That won't be covered in this article.

The important part of the resolver.go file for this article is shown below, comments were added to give a brief explanation

...
//Resolver for mutations
type mutationResolver struct{ *Resolver }

//Create recipe mutation
func (r *mutationResolver) CreateRecipe(ctx context.Context, input *NewRecipe, ingredients []*NewIngredient) (*Recipe, error) {
    panic("not implemented")
}

//Update recipe mutation
func (r *mutationResolver) UpdateRecipe(ctx context.Context, id *int, input *NewRecipe, ingredients []*NewIngredient) (*Recipe, error) {
    panic("not implemented")
}

//Delete recipe mutation
func (r *mutationResolver) DeleteRecipe(ctx context.Context, id *int) ([]*Recipe, error) {
    panic("not implemented")
}


//Query resolver
type queryResolver struct{ *Resolver }

//Get all recipes
func (r *queryResolver) Recipes(ctx context.Context) ([]*Recipe, error) {
    panic("not implemented")
}
...

Now we need to add the logic.

Logic

Firstly go ahead and add a function that allows us to get a database connection from our DB. Add this to the models/models.go:

func FetchConnection() *gorm.DB{
    db,err := gorm.Open("mysql","user:password@/golang")
    if err != nil{
        panic(err)
    }
    return db
}

This is pretty simple, it just simpy opens a connection and returns it. Don't forget to import the gorm package

import(
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

Next add this to the top of your server.go file:

//Migrate Db
    db := models.FetchConnection()
    db.AutoMigrate(&models.Recipe{},&models.Ingredient{})
    db.Close()

Now, we will start with the create resolver. You can edit it like so:

//Create recipe mutation
func (r *mutationResolver) CreateRecipe(ctx context.Context, input *NewRecipe, ingredients []*NewIngredient) (*models.Recipe, error) {
    //Fetch Connection and close db
    db := models.FetchConnection()
    defer db.Close()

    //Create the recipe using the input structs
    recipe := models.Recipe{Name: input.Name, Procedure: *input.Procedure}

    //initialize the ingredients with the length of the input for ingredients
    recipe.Ingredients = make([]models.Ingredient,len(ingredients))
    //Loop and add all items
    for index,item := range ingredients{
        recipe.Ingredients[index] = models.Ingredient{Name: item.Name}
    }
    //Create by passing the pointer to the recipe
    db.Create(&recipe)
    return &recipe, nil
}

Now with this, if you build and run the server.go file. The server should start at localhost:8080. If you visit it you should be greeted with a playground like this playground You can run the mutation query below

# Write your query or mutation here
mutation createRecipe {
  createRecipe(
    input: { name: "Rice stew main", procedure: "First stew it" }
    ingredients: [{ name: "Rice" }, { name: "stew" }]
  ) {
    id
    name
    ingredients {
      name
    }
  }
}

If you run it, you should get the response:

{
  "data": {
    "createRecipe": {
      "id": 1,
      "name": "Jollof RIce",
      "ingredients": [
        {
          "name": "Rice"
        },
        {
          "name": "stew"
        }
      ]
    }
  }
}

Great! We got a basic server running that accepts requests. Now to fill in the rest of the resolver. Fill it

//Resolver for mutations
type mutationResolver struct{ *Resolver }

//Create recipe mutation
func (r *mutationResolver) CreateRecipe(ctx context.Context, input *NewRecipe, ingredients []*NewIngredient) (*models.Recipe, error) {
    //Fetch Connection and close db
    db := models.FetchConnection()
    defer db.Close()

    //Create the recipe using the input structs
    recipe := models.Recipe{Name: input.Name, Procedure: *input.Procedure}

    //initialize the ingredients with the length of the input for ingredients
    recipe.Ingredients = make([]models.Ingredient,len(ingredients))
    //Loop and add all items
    for index,item := range ingredients{
        recipe.Ingredients[index] = models.Ingredient{Name: item.Name}
    }
    //Create by passing the pointer to the recipe
    db.Create(&recipe)
    return &recipe, nil
}

//Update recipe mutation
func (r *mutationResolver) UpdateRecipe(ctx context.Context, id *int, input *NewRecipe, ingredients []*NewIngredient) (*models.Recipe, error) {
    //Fetch Connection and close db
    db := models.FetchConnection()
    defer db.Close()

    var recipe models.Recipe
    //Find recipe based on ID and update
    db = db.Preload("Ingredients").Where("id = ?",*id).First(&recipe).Update("name",input.Name)
    if input.Procedure != nil{
        db.Update("procedure",*input.Procedure)
    }

    //Update Ingredients
    recipe.Ingredients = make([]models.Ingredient,len(ingredients))
    for index,item := range ingredients{
        recipe.Ingredients[index] = models.Ingredient{Name:item.Name}
    }
    db.Save(&recipe)
    return &recipe,nil
}

//Delete recipe mutation
func (r *mutationResolver) DeleteRecipe(ctx context.Context, id *int) ([]*models.Recipe, error) {
    //Fetch connection
    db := models.FetchConnection()
    defer db.Close()
    var recipe models.Recipe

    //Fetch based on ID and delete
    db.Where("id = ?",*id).First(&recipe).Delete(&recipe)

    //Preload and fetch all recipes
    var recipes []*models.Recipe
    db.Preload("Ingredients").Find(&recipes)
    return recipes,nil
}


//Query resolver
type queryResolver struct{ *Resolver }

//Get all recipes
func (r *queryResolver) Recipes(ctx context.Context) ([]*models.Recipe, error) {
    //Fetch a connection
    db := models.FetchConnection()
    //Defer closing the database
    defer db.Close()
    //Create an array of recipes to populate
    var recipes []*models.Recipe
    // .Preload loads the Ingredients relationship into each recipe
    db.Preload("Ingredients").Find(&recipes)
    return recipes,nil
}

Now if you restart the server. You should be able to play around in the playground or via API through the query endpoint.

Conclusions/Things to do

From this tutorial, you can see that golang and graphql are a beautiful match made in heaven seeing as Golang is a statically typed language, and graphql is practically typescript for REST. It's perfect!

Things you can try

-Try using different datatypes, therefore creating custom resolers for objects -Try using a many-to-many relationship for Recipe to Ingredient instead a belongsToMany -Try going wild!

CHEERS!