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 this regard, with detailed OS-specific steps for Linux, Mac and Windows.

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


Creating the project scaffolding

Let’s create a directory for our project and initialize it as a Go module.

$ mkdir go-orders-graphql-api
$ cd go-orders-graphql-api
$ go mod init github.com/[username]/go-orders-graphql-api

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

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

We can 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:

├── 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

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.

After you’ve run the init command above, the graph/schema.graphqls file will contain some auto-generated schema related to a Todo app - We need to add our schema definition to this file (after removing the Todo schema)

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 - concepts 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

The OrderInput and ItemInput types do not have an ID because the ID will 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.


Model definitions

After making changes to the schema, run the following command to generate the models and schema.resolvers.go file afresh. I ran into some issues due to the existing schema definition for the Todo app - deleting the schema.resolvers.go file altogether before running the command solved the problem for me.

go run github.com/99designs/gqlgen generate

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

package model

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"`
}

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.


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 "github.com/jinzhu/gorm"
// In schema.resolver.go
import (
	"context"
	"github.com/soberkoder/go-orders-graphql-api/graph/generated"
	"github.com/soberkoder/go-orders-graphql-api/graph/model"
)
// In server.go
import (
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
	"github.com/soberkoder/go-orders-graphql-api/graph"
	"github.com/soberkoder/go-orders-graphql-api/graph/generated"
	"github.com/soberkoder/go-orders-graphql-api/graph/model"
)

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. 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.

With the below code, we are creating a database named test_db and run a schema migration for tables named orders and items. We may be making changes often to our models, and the Automigrate method should ensure that the database schema is up-to-date with our model definitions (albeit with caveats mentioned below)

# 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{})	
}

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.


Gorm DB connection

For simplicity, I chose to have the DB initialization code in server.go, and we need to pass the gorm DB struct to methods in schema.resolvers.go - This can be done by making a couple of simple changes in schema.resolvers.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))
}

Mutations


1. Create Order mutation

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. 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
}

3. 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.)


Queries


1. 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"
          }
        ]
      }
    ]
  }
}

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.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 generate command to re-generate the models - I consistently end up with issues, and got it working only after deleting the schema.resolvers.go file. I wish the schema re-generation is more seamless, and hope it improves in the upcoming versions of Gqlgen. 2. There were quite a few breaking changes as part of Gqlgen release v0.11.0 (including changes to the directory structure of the auto-generated models, resolvers), and it took a while for me to get the API working with this release.

I found this to be tedious while developing this simple API. With more complex applications in production, the frustration can 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