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 orderCustomerName
- name of the customerOrderAmount
- the total price of all items in the orderItems
- 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 itemItemCode
- code for each itemDescription
- Description of the product/itemQuantity
- 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
- Build a REST API in Golang with MySQL, GORM and Gorilla Mux
- Better logging for GORM and configuring to work with Logrus
- Consuming REST APIs in Go - HTTP GET, PUT, POST and DELETE