Why GraphQL ?
- To avoid multiple versioning of your rest API.
- Ask for what you need: Client has a provision to ask only those fields which they needs. There would be no handling on server side specific to the platform.
- Avoid multiple API calls to get the related data. GraphQL allows us to get the related data in a single request.
- Speed up the application performance.
GraphQL Architecture
Schema
- Schema is the way to provide structure of the object or resource. Based on the schema we can query or mutate the data/resource on the GraphqL server.
Queries
- Queries are used to retrieve the data from the GraphQL server.
Mutations
- Mutations are used to update/modify the resource on the server.
Resolvers
- Resolvers are used to create the data structure that matches with the provided resource schema.
Creating GraphQL api server using golang Gin framework
Case: We are creating a question and choice model. A question can have multiple choices and a choice can have only one question.
Let’s build a GraphQL api server for above scenario using golang packages gqlgen, gorm and gin-gonic
Setup the project
$ mkdir gin-graphql-postgres
$ cd gin-graphql-postgres
$ go mod init github.com/anjaneyulubatta505/gin-graphql-postgres
$ go get github.com/99designs/gqlgen
$ go get -u github.com/gin-gonic/gin
$ go get -u github.com/jinzhu/gorm
Building the server
Let’s create the project skeliton with below command
$ go run github.com/99designs/gqlgen init
It will create a project structure like below
├── 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
If you see the above generated files you can find the schema.graphqls
and schema.resolvers.go
.
schema.graphqls
comes with the ToDO
models by default. We have to update the schema as per our requirement.
Write GraphQL Schema
file: graph/schema.graphqls
type Question{
id: String!
question_text: String!
pub_date: String!
choices: [Choice]
}
type Choice{
id: String!
question: Question!
question_id: String!
choice_text: String!
}
type Query {
questions: [Question]!
choices: [Choice]!
}
input QuestionInput {
question_text: String!
pub_date: String!
}
input ChoiceInput {
question_id: String!
choice_text: String!
}
type Mutation {
createQuestion(input: QuestionInput!): Question!
createChoice(input: ChoiceInput): Choice!
}
Now, let’s run the below command to update the resolvers implementation (i.e By default it comes with TODO app).
$ rm graph/schema.resolvers.go && gqlgen generate
Now, open the file graph/schema.resolvers.go
you can find the resolvers for the above schema.
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"fmt"
"github.com/anjaneyulubatta505/gin-graphql-postgres/graph/generated"
"github.com/anjaneyulubatta505/gin-graphql-postgres/graph/model"
)
func (r *mutationResolver) CreateQuestion(ctx context.Context, input model.QuestionInput) (*model.Question, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) CreateChoice(ctx context.Context, input *model.ChoiceInput) (*model.Choice, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Questions(ctx context.Context) ([]*model.Question, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Choices(ctx context.Context) ([]*model.Choice, error) {
panic(fmt.Errorf("not implemented"))
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
In the above code you can find the functions to implement the queries func (r *queryResolver) Questions
, func (r *queryResolver) Choices
and mutations func (r *mutationResolver) CreateQuestion
, func (r *mutationResolver) CreateChoice
.
Now, we are ready to implement the queries and mutations in GraphQL api.
Database orm setup with “gorm”
Let’s add batteries to communicate with postgres database. Now, create file db/main.go
and the below code.
file: db/main.go
package database
import (
"fmt"
"github.com/AnjaneyuluBatta505/gin-graphql-postgres/graph/model"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
)
type dbConfig struct {
host string
port int
user string
dbname string
password string
}
var config = dbConfig{"localhost", 5432, "postgres", "test", "root"}
func getDatabaseUrl() string {
return fmt.Sprintf(
"host=%s port=%d user=%s dbname=%s password=%s",
config.host, config.port, config.user, config.dbname, config.password)
}
func GetDatabase() (*gorm.DB, error) {
db, err := gorm.Open("postgres", getDatabaseUrl())
return db, err
}
func RunMigrations(db *gorm.DB) {
if !db.HasTable(&model.Question{}) {
db.CreateTable(&model.Question{})
}
if !db.HasTable(&model.Choice{}) {
db.CreateTable(&model.Choice{})
db.Model(&model.Choice{}).AddForeignKey("question_id", "questions(id)", "CASCADE", "CASCADE")
}
}
Now, we are ready to communicate with the database. It’s time to implement the resolvers to return the data that matches with our GraphQL schema.
Implement the GraphQL resolvers
Let’s open the file graph/schema.resolvers.go
and implement the resolvers like below
file: graph/schema.resolvers.go
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"fmt"
"log"
database "github.com/AnjaneyuluBatta505/gin-graphql-postgres/db"
"github.com/AnjaneyuluBatta505/gin-graphql-postgres/graph/generated"
"github.com/AnjaneyuluBatta505/gin-graphql-postgres/graph/model"
)
func (r *mutationResolver) CreateQuestion(ctx context.Context, input model.QuestionInput) (*model.Question, error) {
db, err := database.GetDatabase()
if err != nil {
log.Println("Unable to connect to database", err)
return nil, err
}
defer db.Close()
fmt.Println("input", input.QuestionText, input.PubDate)
question := model.Question{}
question.QuestionText = input.QuestionText
question.PubDate = input.PubDate
db.Create(&question)
return &question, nil
}
func (r *mutationResolver) CreateChoice(ctx context.Context, input *model.ChoiceInput) (*model.Choice, error) {
db, err := database.GetDatabase()
if err != nil {
log.Println("Unable to connect to database", err)
return nil, err
}
defer db.Close()
choice := model.Choice{}
question := model.Question{}
choice.QuestionID = input.QuestionID
choice.ChoiceText = input.ChoiceText
db.First(&question, choice.QuestionID)
choice.Question = &question
db.Create(&choice)
return &choice, nil
}
func (r *queryResolver) Questions(ctx context.Context) ([]*model.Question, error) {
db, err := database.GetDatabase()
if err != nil {
log.Println("Unable to connect to database", err)
return nil, err
}
defer db.Close()
db.Find(&r.questions)
for _, question := range r.questions {
var choices []*model.Choice
db.Where(&model.Choice{QuestionID: question.ID}).Find(&choices)
question.Choices = choices
}
return r.questions, nil
}
func (r *queryResolver) Choices(ctx context.Context) ([]*model.Choice, error) {
db, err := database.GetDatabase()
if err != nil {
log.Println("Unable to connect to database", err)
return nil, err
}
defer db.Close()
db.Find(&r.choices)
return r.choices, nil
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
If you see the above code, we have implemented the mutations
and queries
that returns the data matches with our schema.
In the mutations
implementation we are writing the data to the postgres
database and in the queries
implementation we are retriving the data from the database.
Final touch with Gin-Gonic
We are good to go but we are missing the speed of Gin-Gonic. Let’s do that. Open the file server.go
and update it like below.
file: server.go
package main
import (
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/AnjaneyuluBatta505/gin-graphql-postgres/graph"
"github.com/AnjaneyuluBatta505/gin-graphql-postgres/graph/generated"
"github.com/gin-gonic/gin"
)
const defaultPort = ":8080"
// Defining the Graphql handler
func graphqlHandler() gin.HandlerFunc {
// NewExecutableSchema and Config are in the generated.go file
// Resolver is in the resolver.go file
h := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
return func(c *gin.Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}
// Defining the Playground handler
func playgroundHandler() gin.HandlerFunc {
h := playground.Handler("GraphQL", "/query")
return func(c *gin.Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}
func main() {
r := gin.Default()
r.POST("/query", graphqlHandler())
r.GET("/", playgroundHandler())
r.Run(defaultPort)
}
Yeah! We are ready to test GraphQL api server. Let’s launch it with below command.
$ go run server.go
It will run the server on “localhost:8080
”. Let’s open that url to see the GraphQL playground.
GraphQL query to mutate the data
Let’s write the GraphQL query to create the data in the database through playground
mutation {
createQuestion(input: {question_text: "What is your name ?", pub_date: "2020-04-27"}){
id
question_text
}
}
After executing the above query we will get the JSON data like below (i.e not exactly same :P)
{
"data": {
"createQuestion": {
"id": "3",
"question_text": "What is your name ?"
}
}
}
In the above we are creating the question with data {question_text: "What is your name ?", pub_date: "2020-04-27"}
and after creating we are asking it to return id
, question_text
. If we want only id then we can remove question_text
from the query.
Find the equivalent cURL
request below
curl 'http://localhost:8080/query' \
-H 'Connection: keep-alive' \
-H 'accept: */*' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36' \
-H 'content-type: application/json' \
-H 'Origin: http://localhost:8080' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Dest: empty' \
-H 'Referer: http://localhost:8080/' \
-H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8'\
--data-binary '{"operationName":null,"variables":{},"query":"mutation {\n createQuestion(input: {question_text: \"What is your name ?\", pub_date: \"2020-04-27\"}) {\n id\n question_text\n }\n}\n"}' \
--compressed
You can import the above cURL into postman to test it.
In the same way we can wtite the query to insert the choices. Let’s an example GraphQL query.
mutation {
createChoice(input: {question_id: "3", choice_text: "Agiliq"}){
id
question{
id
question_text
}
choice_text
}
}
Till now, we have seen how to mutate the data. Let’s see how to retrieve the data from GraphQL api server.
GraphQL query to query/retrieve the data
Let’s write a graphql query to retrieve all the questions with their options.
query{
questions{
id
question_text
choices{
id
choice_text
}
}
}
It will give us the JSON response like below.
{
"data": {
"questions": [
{
"id": "3",
"question_text": "What is your name ?",
"choices": [
{
"id": "31",
"choice_text": "Agiliq"
}
]
}
]
}
}
You can check-out the source code at Github - gin-graphql-postgres
That’s it folks, we can do more with GraphQL. You can read more in these GraphQL tutorials we have written.
- Getting started with Python graphene: How to write GraphQL compliant apis with Python
- Using python graphene with Django: How to write GraphQL compliant apis with Djangohttps://www.agiliq.com/blog/2019/09/python-graphene-with-django/
Thank you for reading the Agiliq blog. This article was written by Anjaneyulu Batta on Apr 27, 2020 in Golang , GraphQL .
You can subscribe ⚛ to our blog.
We love building amazing apps for web and mobile for our clients. If you are looking for development help, contact us today ✉.
Would you like to download 10+ free Django and Python books? Get them here