Golang and graphql: Building a recipe CRUD api with golang, graphql and mysql
Add to bookmarksSat 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
- Basic understanding of graphql and it's schema definition language
- Basic understanding of golang and MySQL databases
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:[email protected]/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
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!