Build a GraphQL API in Golang with MySQL and GORM using Gqlgen


golang graphql api gorm

In a previous post - Go REST API with GORM and MySQL, we saw how to build a simple REST service in Golang with MySQL and the GORM framework for object-relational mapping. This is a similar article, where we will be discussing how to build a GraphQL API with MySQL database and GORM. We will be using a Go library called Gqlgen which simplifies the development process by auto-generating a lot of the boilerplate code, thereby letting us focus on the application logic.


Initial Setup

The first pre-requisite for getting this working is to have a working environment setup for Go. As long as you have installed Go, and validated your setup , we should be good to go. Also take a look at the code organization guidelines, to get a clear idea about Go modules and packages.

The second requirement is to have MySQL database installed. The official MySQL documentation is the best resource in the regard, with detailed OS-specific steps for Linux, Mac and Windows.

The next step is to download and install the dependent packages - namely, Gorm, and Go-sql-driver for MySql . Run the following commands from the terminal:

go get -u github.com/jinzhu/gorm
go get -u github.com/go-sql-driver/mysql

The article also assumes the readers are familiar with GraphQL and GraphQL-related terminologies (query, mutation , schema, etc).


GraphQL Schema definition

Gqlgen is a schema-first library - Now what does this mean? This means that we first need to define a schema/type system for our data before writing code to implement the API. The Go models/structs, resolver skeleton-code is auto-generated by Gqlgen using the schema definition.

Graphql uses types to describe the set of possible data a client can query from the server - This ensures clients only ask for what’s possible and are met with errors otherwise. GraphQL has its own schema definition language (called a GraphQL schema language (or GraphQL schema definition language - SDL) ) which lets us define our schema in a language-agnostic manner. Under the root folder of the project, let’s create a new file called schema.graphql which will house our schema definitions.

type Order {
    id: Int!
    customerName: String!
    orderAmount: Float!
    items: [Item!]!
}

type Item {
    id: Int!
    productCode: String!
    productName: String!
    quantity: Int!
}

input OrderInput {
    customerName: String!
    orderAmount: Float!
    items: [ItemInput!]!
}

input ItemInput {
    productCode: String!
    productName: String!
    quantity: Int!
}

type Mutation {
    createOrder(input: OrderInput!): Order!
    updateOrder(orderId: Int!, input: OrderInput!): Order!
    deleteOrder(orderId: Int!): Boolean!
}

type Query {
    orders: [Order!]!
}

I chose this schema with a one-to-many relationship since it would help us understand how to deal with foreign keys , associations between models, eager loading - ideas which are essential to understand while dealing with object -relational mapping.

An order has the following fields defined inside the Order type.

  • id - id for each order

  • CustomerName - name of the customer

  • OrderAmount - the total price of all items in the order

  • Items - list of items in the order. An order can have one or more items, and this is an example of one-to-many relationship/association between entities

(The exclamation mark indicates that the field cannot be null - For example, it doesn’t make sense to have an order without a customer or items, hence they are marked as not-null)

Similarly, each individual item in an order has the following fields declared in the Item struct.

  • id - id for each item

  • ItemCode - code for each item

  • Description - Description of the product/item

  • Quantity - Number of quantities of the item in the order

It’s recommended to use a different type for input objects, since the fields required for inputs could be optional for the

The OrderInput and ItemInput types do not have an ID because the ID is supposed to be generated by the server and the clients aren’t supposed to pass it.

Why do we need a separate OrderInput type, and why can’t the Order type be re-used for the input argument for the createOrder mutation?

I think the following extract from the official GraphQL spec answers this question.

*The GraphQL Object type can contain fields that define arguments or contain references to interfaces and unions , neither of which is appropriate for use as an input argument. For this reason, input objects have a separate type in the system.*


Creating the project scaffolding

Once we have the types defined, it’s time to generate a skeleton for the project by running the following command:

go run github.com/99designs/gqlgen init

We should be seeing the following files created under our project directory:

server/server.go // Minimal entry point to our graphQL server. Run this to start the server
generated.go // The GraphQL execution runtime
go.mod // Generate mod file
go.sum // Generated mod file
gqlgen.yml // Yaml file that holds config data for gqlgen (For eg. location of models, schema, resolvers, etc)
models_gen.go // Holds model to graphql types
resolver.go // contains our application code, with un-implemented resolvers methods
schema.graphql // Schema file we created

Tweaking our Models

The auto-generated model structs are placed in models_gen.go file, and it looks like the following for our application.

package go_orders_graphql_api

type ItemInput struct {
	ProductCode string `json:"productCode"`
	ProductName string `json:"productName"`
	Quantity    int    `json:"quantity"`
}

type OrderInput struct {
	CustomerName string       `json:"customerName"`
	OrderAmount  float64      `json:"orderAmount"`
	Items        []*ItemInput `json:"items"`
}

type Item struct {
	ID          int `json:"id"`
	ProductCode string `json:"productCode"`
	ProductName string `json:"productName"`
	Quantity    int    `json:"quantity"`
}

type Order struct {
	ID           int  `json:"id"`
	CustomerName string  `json:"customerName"`
	OrderAmount  float64 `json:"orderAmount"`
	Items        []Item `json:"items"`
}

Often, we may have to customize/override the generated models to cater to our use case - For example: Since we are using GORM as our ORM, we would need to add GORM-specific tags to our Order and Item struct. Hence it makes sense to create our own struct, and instruct gqlgen to use them instead of the auto-generated ones.

Let’s create a new directory called models and place our model structs for Order and Item

# In models/order.go
package models

// Order model
type Order struct {
    ID           int     `json:"id" gorm:"primary_key"`
    CustomerName string  `json:"customerName"`
    OrderAmount  float64 `json:"orderAmount"`
    Items        []Item  `json:"items" gorm:"foreignkey:OrderID"`
}
# In models/item.go
package models

// Item model
type Item struct {
    ID          int `json:"id" gorm:"primary_key"`
    ProductCode string `json:"productCode"`
    ProductName string `json:"productName"`
    Quantity    int    `json:"quantity"`
    OrderID     int    `json:"-"`
}

Next, we need to instruct gqlgen to use this by adding an entry to the gqlgen.yml file to refer to the new models we created.

models:
  Order:
    model: github.com/[username]/go-orders-graphql-api/models.Order
  Item:
    model: github.com/[username]/go-orders-graphql-api/models.Item

Eventually, the gqlgen.yaml file should look something like the following:

# .gqlgen.yml example
#
# Refer to https://gqlgen.com/config/
# for detailed .gqlgen.yml documentation.

schema:
- schema.graphql
exec:
  filename: generated.go
model:
  filename: models_gen.go
resolver:
  filename: resolver.go
  type: Resolver
models:
  Order:
    model: github.com/soberkoder/go-orders-graphql-api/models.Order
  Item:
    model: github.com/soberkoder/go-orders-graphql-api/models.Item
autobind: []

Just a quick note about GORM naming conventions with respect to our models:

Table name is the pluralized version of struct name. (If the model struct is named Order, the table name will be orders)

Column names will be the field’s name is lower snake case. (If the struct field is named CustomerName, the column name will be customer_name)

As you can see, the field ID has the gorm:"primary_key" tag - This means that the id column will be the primary key for the orders table once created. Integer fields with primary_key tag are auto_increment by default. - This fits in well for our use-case and we don’t have to worry about generating a unique id for each order.

The Items field has the gorm:"foreignKey:ID" tag - This means that the items table will have an id column that references the id column in the orders table.

Also, the id field is marked as the primary key for the Items model, and will serve as the unique identifier for records in the items table.

$ rm resolver.go
$ go run github.com/99designs/gqlgen

Imports

Now that we have our environment setup, it’s time to write some code and get our hands dirty. Let’s start by importing the set of packages mentioned in the code snippet below:

// In resolver.go
import (
    "context"

    "github.com/jinzhu/gorm"
    "github.com/soberkoder/go-orders-graphql-api/models"
)
// In server.go
import (
    "log"
    "net/http"
    "os"

    "fmt"
    "github.com/99designs/gqlgen/handler"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    go_orders_graphql_api "github.com/soberkoder/go-orders-graphql-api"
    "github.com/soberkoder/go-orders-graphql-api/models"
)

fmt - This package implements formatted I/O functions similar to scanf and printf in C

log - Has methods for formatting and printing log messages.

net/http - Contains methods for performing operations over HTTP. It provides HTTP server and client implementations and has abstractions for HTTP request, response, headers, etc.

github.com/jinzhu/gorm - Gorm is the most widely-used framework for object-relational mapping in Go

github.com/jinzhu/gorm/dialects/mysql - GORM has wrapped some drivers to make it easier to remember the import path . This internally refers the github.com/go-sql-driver/mysql driver we installed earlier.


Initial DB setup and configuration

For the purpose of this tutorial, it would be preferable to create the database and tables with GORM.

# In servers/server.go
var db *gorm.DB;

func initDB() {
    var err error
    dataSourceName := "root:@tcp(localhost:3306)/?parseTime=True"
    db, err = gorm.Open("mysql", dataSourceName)

    if err != nil {
        fmt.Println(err)
        panic("failed to connect database")
    }

    db.LogMode(true)

    // Create the database. This is a one-time step.
    // Comment out if running multiple times - You may see an error otherwise
    db.Exec("CREATE DATABASE test_db")
    db.Exec("USE test_db")

    // Migration to create tables for Order and Item schema
    db.AutoMigrate(&models.Order{}, &models.Item{})	
}

Since multiple methods in our code will require database access (createOrder, getOrder, etc as we will see shortly), it is a good idea to have a global variable for the database connectivity and a separate function that initializes during application startup.

Note:

AutoMigrate will ONLY create tables, missing columns and missing indexes, and WON’T change existing column’s type or delete unused columns to protect your data.

If you are playing around with the Order or Item model, by changing the datatype of fields or by removing fields , it will not reflected in the DB tables. In that case, it’s better to delete the tables manually, and run the migration again to create tables with the updated model.


Initialize DB connection

For simplicity, I chose to have the DB initialization code in server.go, and we need pass the gorm DB struct to methods in resolver.go - This can be done by making a couple of simple changes in resolver.go and server.go.

# In resolver.go
type Resolver struct{
    DB *gorm.DB
}
# In servers/server.go
func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }

    initDB()
    http.Handle("/", handler.Playground("GraphQL playground", "/query"))
    http.Handle("/query", handler.GraphQL(go_orders_graphql_api.NewExecutableSchema(go_orders_graphql_api.Config{Resolvers: &go_orders_graphql_api.Resolver{
        DB: db,
    }})))

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

1. Create API

Now, let’s code the API for creating an order.

func (r *mutationResolver) CreateOrder(ctx context.Context, input OrderInput) (*models.Order, error) {
    order := models.Order {
        CustomerName: input.CustomerName,
        OrderAmount: input.OrderAmount,
        Items: mapItemsFromInput(input.Items),
    }
    r.DB.Create(&order)
    return &order, nil
}

The implementation is pretty straightforward - First off, we map the OrderInput struct to the Order model struct we created, along with the list of input items. This bit of code to map the items list can be re-used by the UpdateOrder mutation as wellIt makes sense extract the logic to a separate function - The mapItemsFromInput function below iterates through the items in the input and maps them to the Item model struct.

func mapItemsFromInput(itemsInput []*ItemInput) ([]models.Item) {
    var items []models.Item
    for _, itemInput := range itemsInput {
        items = append(items, models.Item{
            ProductCode: itemInput.ProductCode,
            ProductName: itemInput.ProductName,
            Quantity: itemInput.Quantity,
        })
    }
    return items
}

As you might have noticed, the CreateOrder implementation is missing any error-handling logic - We can improve this slightly, by adding a simple check for errors from GORM.

func (r *mutationResolver) CreateOrder(ctx context.Context, input OrderInput) (*models.Order, error) {
    order := models.Order {
        CustomerName: input.CustomerName,
        OrderAmount: input.OrderAmount,
        Items: mapItemsFromInput(input.Items),
    }
	err := r.DB.Create(&order).Error
    if err != nil {
        return nil, err
    }
    return &order, nil
}

2. Get Orders query

The Orders query is intended to fetch and return all orders in the database.

func (r *queryResolver) Orders(ctx context.Context) ([]*models.Order, error) {	
    var orders []*models.Order
    r.DB.Preload("Items").Find(&orders)
    
    return orders, nil
}

The getOrders function is relatively simple - We use the db.Find() method to fetch all the orders, which is then returned as a response. The db.Preload() method ensures that associations are preloaded while using the Find () method. In our case, we would like to see the item details we well, when we lookup the order. Hence we use the Preload() method to fetch the associated items, in addition to the order details.

As shown below, the items attribute in the json will be empty, if we had not used the Preload() method for loading the associated items.

Without Preloading:

{
  "data": {
    "orders": [
      {
        "id": 1,
        "items": []
      }
    ]
  }
}

With Preloading:

{
  "data": {
    "orders": [
      {
        "id": 1,
        "items": [
          {
            "productCode": "2323"
          }
        ]
      }
    ]
  }
}

3. Update Order mutation

Next, we are going to see how to update the details of an order. The orderID param refers to the order that needs to be updated. The input param contains the updated details of the order. Similar to the CreateOrder mutation. map the OrderInput struct to the Order model we created. Once we have the updated order details in the struct , we can use the db.Save() method to update the records in the DB.

By default, the save command will update the associated entities having a primary key. In our case, since the associated Items model has the ID as primary key, the db.Save() command will automatically update the related records in the items table as well.

func (r *mutationResolver) UpdateOrder(ctx context.Context, orderID int, input OrderInput) (*models.Order, error) {
    updatedOrder := models.Order {
        ID: orderID,
        CustomerName: input.CustomerName,
        OrderAmount: input.OrderAmount,
        Items: mapItemsFromInput(input.Items),
    }
    r.DB.Save(&updatedOrder)
    return &updatedOrder, nil
}

4. Delete Order mutation

Finally, we have the delete mutation. We read the orderID, identify matching records using the Where function and delete them using the Delete function.

In the below code, we end up calling the Delete() method twice - once for deleting the items, and the next for deleting the order. The reason is that Cascade on delete is (i.e. automatically deleting associated items when an order is deleted) is not implemented in GORM yet. From what I understand, Gorm tag for cascading deletes is still being worked on and I could not find a simpler approach for deleting associated objects from the database.

func (r *mutationResolver) DeleteOrder(ctx context.Context, orderID int) (bool, error) {
    r.DB.Where("order_id = ?", orderID).Delete(&models.Item{})
    r.DB.Where("order_id = ?", orderID).Delete(&models.Order{})
    return true, nil;
}

(Ideally, the deletion of the items and the order should be wrapped in a transaction but I haven’t done that here to keep the code concise.)


Running & Testing the App

Finally, we are done with all the APIs, and it’s time take them for a spin. To run the app, navigate to your project directory, and run the following command:

go run server/server.go

Navigate to http://localhost:8080/ on your browser to open GraphQL playground

Create Order

mutation createOrder ($input: OrderInput!) {
  createOrder(input: $input) {
    id
    customerName
    items {
      id
      productCode
      productName
      quantity
    }
  }
}

Query Variables:

{
  "input": {
    "customerName": "Leo",
    "orderAmount": 9.99,
    "items": [
      {
      "productCode": "2323",
      "productName": "IPhone X",
      "quantity": 1
      }
    ]
  }
}

Get Orders

query orders {
  orders {
    id  
    customerName
    items {
      productName
      quantity
    }
  }
}

Update Order

mutation updateOrder ($orderId: Int!, $input: OrderInput!) {
  updateOrder(orderId: $orderId, input: $input) {
    id
    customerName
    items {
      id
      productCode
      productName
      quantity
    }
  }
}

Query variables:

{
  "orderId":1,
  "input": {
    "customerName": "Cristiano",
    "orderAmount": 9.99,
    "items": [
      {
      "productCode": "2323",
      "productName": "IPhone X",
      "quantity": 1
      }
    ]
  }
}

Delete Order

mutation deleteOrder ($orderId: Int!) {
  deleteOrder(orderId: $orderId)
}

Query variables:

{
  "orderId": 3
}

You can also use tools like Insomnia and Altair to try these requests out.


Pain points

Although Gqlgen simplifies the development of GraphQL servers in Golang, the developer experience(or DX as it’s called these days) is not entirely seamless, in my opinion. 1. Whenever there is a schema change, we’ll have to run the gqlgen init command to re-generate the models 2. Whenever we tweak the models, we’ll have to regenerate the resolvers by running the following command:

go run github.com/99designs/gqlgen -v

This only works if the resolver.go file doesn’t exist in our project, which isn’t really possible if we have some existing implementations for our queries and mutations. The best option is to delete the resolver.go file after copying its contents, run the above command and copy function implementations back.

I found this to be really tedious while developing this simple API. With more complex applications in production , the frustration will really compound.


Next steps

If you enjoyed building this GraphQL application and are hungry for more, the following are some things you can try implementing :) - More complex objects - Add more types like charges, discounts, taxes for each item - Pagination - Paginate Orders query to fetch a limited number of orders/items - Filtering - Try adding some filter params and implement filtering in the Orders query (For eg. filter orders by customer, orders with certain items, etc)


Conclusion

In this post, we saw how 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 hope you go on to build much cooler/bigger/better APIs in Golang. You can checkout the complete code for this API from Github. Please do comment below if you have any questions/issues with the code - I’ll be glad to help out.


See Also