What is GraphQL? And How is it Implemented in Golang?

https://graphql.org/

Over the years, you’ve been using REST API for your development needs. However, this service doesn’t allow developers to use flexibility without making a large collection of unnecessary calls. For example, if the data required by web and mobile are different, we also have to create two different endpoints, specifically for web and mobile.

Therefore, Facebook creates a query language called GraphQL, which gives developers the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

In this article, I will highlight the main features related to GraphQL along with discussing the significant advantages and disadvantages associated with this particular API specification. And in the end, I will present a simple program using Golang that has implemented GraphQL.

What is GraphQL?

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data.

GraphQL is a query language that can be used anywhere, but it often bridges the gap between client and server applications. It isn’t opinionated about which network layer is used, so data can be read and written between client and server applications — Robin Wieruch, The Road to GraphQL

Although a Query Language but GraphQL is not directly related to the database, you can say that GraphQL is not limited to certain databases, either SQL or NoSQL. GraphQL position is implemented on the client and server-side which is connected/accessing an API. One of the objectives of developing this query language is to facilitate data communication between the back-end and front-end or mobile applications by giving them only the data they need.

Operation in GraphQL

1. Query

The query is used to reading or fetching values. In either case, the operation is a simple string that a GraphQL server can parse and respond to with data in a specific format.

You can use the query operation to request data from an API. A query describes the data that you want to fetch from a GraphQL server. When you send a query, you ask for units of data by field — Learning GraphQL by Eve Porcello and Alex Banks

2. Schema

GraphQL uses a schema to describe the shape of your data graph.
This schema defines a hierarchy of types with fields that are populated from your back-end data stores. The schema also specifies exactly which queries and mutations are available for clients to execute against your data graph.

3. Resolver

A resolver is a function that’s responsible for populating the data for a single field in your schema. It can populate that data in any way you define, such as by fetching data from a back-end database or a third-party API.

4. Mutation

Modify data in the data store and returns a value. It can be used to insert, update, or delete data.

The mutation shares the same principles as the query: it has fields and objects, arguments and variables, fragments and operation names, as well as directives and nested objects for the returned result — Robin Wieruch, The Road to GraphQL

5. Subscription

A way to push data from the server to the clients that choose to listen to real-time messages from the server.

Subscriptions in GraphQL came from a real-life use case at Facebook. The team wanted a way to show real-time information about the number of likes (Live Likes) that a post was getting without refreshing the page — Learning GraphQL by Eve Porcello and Alex Banks

GraphQL Advantages and Disadvantages

Photo From AltexSoft

Advantages

1. Fast Develop
We take a case, how to get a book borrower. In the view, first I want to display the book list, then in the book list menu display, inside there is a list of people who are borrowing the book.

So in REST API, we need to make a new endpoint to return the book list and a new endpoint to return the borrower in each of these books.

Different from REST API, in GraphQL it is enough to only use one endpoint to return a list of books, and a list of borrowers.

Using this example GraphQL query:

2. Flexible
We take a case, how to get book details. On the web view, I want to show the book details such as name, price, and description.

So in REST API, we need to make a new endpoint to return the book details such as name, price, and description.

REST API Scheme

What if on the mobile view, I just want to show the book details like name and price? If I use the same endpoint as the web view, there will be wasted data which is the description. Therefore I will change the existing logic inside that endpoint, or create a new endpoint.

GraphQL Scheme

Different from REST API, in GraphQL it is enough to only use one endpoint to return book details according to the web or mobile needs. In GraphQL you just change the query.

3. Easy to Use at Simple Maintain
So we saw that graph well was a fast development and a flexible graph is also easy to use and simple to maintain.

  • Rest API
    If the client needs different data. It typically requires us to add a new endpoint or change an existing one.
  • GraphQL
    The client just needs to change its query.

Disadvantages

  1. Handling File Upload — There is nothing about file upload in the GraphQL specification and mutations don’t accept files in the arguments.
  2. Simple API’s — In case you have a service that exposes a really simple API, GraphQL will only add extra complexity, so a simple REST API can be better.

Code Implementation

In the implementation, I use the Golang programming language, along with the project structure:

For the dependency version and dependency management features, I am using go modules. To supports: queries, mutations & subscriptions, I am using graphql-go. And for the handler, I am using graphql-go-handler.

At this time I will create a simple program, where I will create a CRUD for detailed books using GraphQL. For the steps below:

Firstly, create an environment folder, then create a file called .

app:
name: "GraphQL Test"
debug: true
port: "8080"
host: "localhost"
service: "http"
context:
timeout: 2
databases:
mongodb:
name: "local_db"
connection: "mongodb://root:root@localhost:27017"

Secondly, create an infrastructure folder, then create some file called , , and . The purpose of this folder, to configure the database and read data from .

databaseConfiguration.go

package infrastructureimport (
"context"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"log"
)
var Mongodb *mongo.Databasefunc (e *Environment) InitMongoDB() (db *mongo.Database, err error) {
clientOptions := options.Client().ApplyURI(e.Databases["mongodb"].Connection)
client, err := mongo.Connect(context.TODO(), clientOptions)
err = client.Ping(context.TODO(), nil)
if err != nil {
return db, err
}
Mongodb = client.Database(e.Databases["mongodb"].Name)
log.Println("Mongodb Ready!!!")
return db, err
}

environmentConfiguration.go

package infrastructureimport (
"io/ioutil"
"log"
"os"
"path"
"runtime"
"gopkg.in/yaml.v2"
)
func (env *Environment) SetEnvironment() {
_, filename, _, _ := runtime.Caller(1)
env.path = path.Join(path.Dir(filename), "environment/Connection.yml")
_, err := os.Stat(env.path)
if err != nil {
panic(err)
return
}
}
func (env *Environment) LoadConfig() {
content, err := ioutil.ReadFile(env.path)
if err != nil {
log.Println(err)
panic(err)
}
err = yaml.Unmarshal([]byte(string(content)), env)
if err != nil {
log.Println(err)
panic(err)
}
if env.App.Debug == false {
log.SetOutput(ioutil.Discard)
}
log.Println("Config load successfully!")
return
}

model.go

package infrastructuretype app struct {
Appname string `yaml:"name"`
Debug bool `yaml:"debug"`
Port string `yaml:"port"`
Service string `yaml:"service"`
Host string `yaml:"host"`
}
type database struct {
Name string `yaml:"name"`
Connection string `yaml:"connection"`
}
type Environment struct {
App app `yaml:"app"`
Databases map[string]database `yaml:"databases"`
path string
}

Thirdly, create a book folder, then create some files like this:

model.go

package booktype Book struct {
Name string
Price string
Description string
}

resolver.go

package bookimport (
"context"
"github.com/graphql-go/graphql"
)
var productType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Book",
Fields: graphql.Fields{
"name": &graphql.Field{
Type: graphql.String,
},
"price": &graphql.Field{
Type: graphql.String,
},
"description": &graphql.Field{
Type: graphql.String,
},
},
},
)
var queryType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"book": &graphql.Field{
Type: productType,
Description: "Get book by name",
Args: graphql.FieldConfigArgument{
"name": &graphql.ArgumentConfig{
Type: graphql.String,
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
var result interface{}
name, ok := p.Args["name"].(string)
if ok {
// Find product
result = GetBookByName(context.Background(), name)
}
return result, nil
},
},
"list": &graphql.Field{
Type: graphql.NewList(productType),
Description: "Get book list",
Args: graphql.FieldConfigArgument{
"limit": &graphql.ArgumentConfig{
Type: graphql.Int,
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
var result interface{}
limit, _ := params.Args["limit"].(int)
result = GetBookList(context.Background(), limit)
return result, nil
},
},
},
})
var mutationType = graphql.NewObject(graphql.ObjectConfig{
Name: "Mutation",
Fields: graphql.Fields{
"create": &graphql.Field{
Type: productType,
Description: "Create new book",
Args: graphql.FieldConfigArgument{
"name": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"price": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"description": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
book := Book{
Name: params.Args["name"].(string),
Price: params.Args["price"].(string),
Description: params.Args["description"].(string),
}
if err := InsertBook(context.Background(), book); err != nil {
return nil, err
}
return book, nil
},
},
"update": &graphql.Field{
Type: productType,
Description: "Update book by name",
Args: graphql.FieldConfigArgument{
"name": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"price": &graphql.ArgumentConfig{
Type: graphql.String,
},
"description": &graphql.ArgumentConfig{
Type: graphql.String,
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
book := Book{}
if name, nameOk := params.Args["name"].(string); nameOk {
book.Name = name
}
if price, priceOk := params.Args["price"].(string); priceOk {
book.Price = price
}
if description, descriptionOk := params.Args["description"].(string); descriptionOk {
book.Description = description
}
if err := UpdateBook(context.Background(), book); err != nil {
return nil, err
}
return book, nil
},
},
"delete": &graphql.Field{
Type: productType,
Description: "Delete book by name",
Args: graphql.FieldConfigArgument{
"name": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
name, _ := params.Args["name"].(string)
if err := DeleteBook(context.Background(), name); err != nil {
return nil, err
}
return name, nil
},
},
},
})
// schema
var Schema, _ = graphql.NewSchema(
graphql.SchemaConfig{
Query: queryType,
Mutation: mutationType,
},
)

repository.go

package bookimport (
"context"
"log"
"graphql/infrastructure""go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
func GetBookByName(ctx context.Context, name string) (result interface{}) {
var book Book
data := infrastructure.Mongodb.Collection("booklist").FindOne(ctx, bson.M{"name": name})
data.Decode(&book)
return book
}
func GetBookList(ctx context.Context, limit int) (result interface{}) {
var book Book
var books []Book
option := options.Find().SetLimit(int64(limit))cur, err := infrastructure.Mongodb.Collection("booklist").Find(ctx, bson.M{}, option)
defer cur.Close(ctx)
if err != nil {
log.Println(err)
return nil
}
for cur.Next(ctx) {
cur.Decode(&book)
books = append(books, book)
}
return books
}
func InsertBook(ctx context.Context, book Book) error {
_, err := infrastructure.Mongodb.Collection("booklist").InsertOne(ctx, book)
return err
}
func UpdateBook(ctx context.Context, book Book) error {
filter := bson.M{"name": book.Name}
update := bson.M{"$set": book}
upsertBool := true
updateOption := options.UpdateOptions{
Upsert: &upsertBool,
}
_, err := infrastructure.Mongodb.Collection("booklist").UpdateOne(ctx, filter, update, &updateOption)
return err
}
func DeleteBook(ctx context.Context, name string) error {
_, err := infrastructure.Mongodb.Collection("booklist").DeleteOne(ctx, bson.M{"name": name})
return err
}

response.go

package bookimport (
"encoding/json"
"net/http"
"time"
)
type SetResponse struct {
Status string `json:"status"`
Data interface{} `json:"data,omitempty"`
AccessTime string `json:"accessTime"`
}
func HttpResponseSuccess(w http.ResponseWriter, r *http.Request, data interface{}) {
setResponse := SetResponse{
Status: http.StatusText(200),
AccessTime: time.Now().Format("02-01-2006 15:04:05"),
Data: data}
response, _ := json.Marshal(setResponse)
w.Header().Set("Content-Type", "Application/json")
w.WriteHeader(200)
w.Write(response)
}
func HttpResponseError(w http.ResponseWriter, r *http.Request, data interface{}, code int) {
setResponse := SetResponse{
Status: http.StatusText(code),
AccessTime: time.Now().Format("02-01-2006 15:04:05"),
Data: data}
response, _ := json.Marshal(setResponse)
w.Header().Set("Content-Type", "Application/json")
w.WriteHeader(code)
w.Write(response)
}

routes.go

package bookimport (
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/graphql-go/handler"
)
func RegisterRoutes(r *chi.Mux) *chi.Mux {
/* GraphQL */
graphQL := handler.New(&handler.Config{
Schema: &Schema,
Pretty: true,
GraphiQL: true,
})
r.Use(middleware.Logger)
r.Handle("/query", graphQL)
return r
}

And finally, create a file named .

main.go

package mainimport (
"github.com/go-chi/chi"
"graphql/book"
"graphql/infrastructure"
"log"
"net/http"
"net/url"
)
func main() {
routes := chi.NewRouter()
r := book.RegisterRoutes(routes)
log.Println("Server ready at 8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
func init() {
val := url.Values{}
val.Add("parseTime", "1")
val.Add("loc", "Asia/Jakarta")
env := infrastructure.Environment{}
env.SetEnvironment()
env.LoadConfig()
env.InitMongoDB()
}

If we run the program, the result will be like this:

Example Create Book Details

Conclusion

Of all the benefits GraphQL has (fast developing, flexible, easy to use, and simple to maintain). But it turns out GraphQL has disadvantages in handling file uploads and the performance of the Simple API compared to the REST API. So, we have to look first to know the system that we are going to build, whether it is suitable to implement GraphQL as the design architecture of our application.

References

Tunaiku Tech

Stories behind Tunaiku Products, Engineering and Data Team