GraphQL API (CRUD) on Go
- Tutorial
Hello! About GraphQL many articles on Habré, but running over them found that they all bypass such a wonderful language as Go. Today I will try to correct this misunderstanding. To do this, we write an API on Go using GraphQL.
In short, GraphQL is a query language for building an API that describes how to request and return data (for more information, see the official graphql.github.io resource and on the habr ).
Argue that graphQL or REST are better here
We will have a classic API: CRUD (Create, Read, Update, Delete) adding, receiving, editing and deleting products in the online store.
On the server side, we will use the ready-made graphql-go implementation of GraphQL
First you need to download graphql-go, this can be done with the command
go get github.com/graphql-go/graphql
Next, we describe the structure of the goods (in simplified form)
type Product struct {
ID int64`json:"id"`
Name string`json:"name"`
Info string`json:"info,omitempty"`
Price float64`json:"price"`
}
ID
- unique identifier, Name
- name, Info
- product information, Price
- price
The first thing that needs to be done is to call a method Do
that takes the data schema and the request parameters as input parameters. And returns us the resulting data (for further transmission to the client)
result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
})
funcexecuteQuery(query string, schema graphql.Schema) *graphql.Result {
result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
})
iflen(result.Errors) > 0 {
fmt.Printf("errors: %v", result.Errors)
}
return result
}
funcmain() {
http.HandleFunc("/product", func(w http.ResponseWriter, r *http.Request) {
result := executeQuery(r.URL.Query().Get("query"), schema)
json.NewEncoder(w).Encode(result)
})
http.ListenAndServe(":8080", nil)
}
Schema
- data scheme, RequestString
- the value of the query string parameter, in our case the valuequery
Schema
The scheme accepts two root data types: Query
- immutable data, Mutation
- mutable data
var schema, _ = graphql.NewSchema(
graphql.SchemaConfig{
Query: queryType,
Mutation: mutationType,
},
)
Query (Queries)
Query
serves to read (and read only) data. With the help Query
we specify what data the server should return.
We write the implementation of the data type Query
, in our case it will contain fields with information about a single product (product) and a list of goods (list)
var queryType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
/* Получение продукта по ID
http://localhost:8080/product?query={product(id:1){name,info,price}}
*/"product": &graphql.Field{
Type: productType,
Description: "Get product by id",
// Получаем список аргументов, для дальнейшего использования
Args: graphql.FieldConfigArgument{
// В данном случае нам необходим только id"id": &graphql.ArgumentConfig{
Type: graphql.Int,
},
},
Resolve: func(p graphql.ResolveParams)(interface{}, error) {
id, ok := p.Args["id"].(int)
if ok {
// Поиск продукта с IDfor _, product := range products {
ifint(product.ID) == id {
return product, nil
}
}
}
returnnil, nil
},
},
/* Получение списка продуктов
http://localhost:8080/product?query={list{id,name,info,price}}
*/"list": &graphql.Field{
Type: graphql.NewList(productType),
Description: "Get product list",
Resolve: func(params graphql.ResolveParams)(interface{}, error) {
return products, nil
},
},
},
})
The queryType type contains mandatory fields Name
and Fields
, as well as optional Description
(used for documentation).
In turn, the field Fields
also contains a required field Type
and optional fields Args
, Resolve
andDescription
Args
Arguments - the list of parameters transferred from the client to the server and affecting the result of the returned data. Arguments are tied to a specific field. Moreover, the arguments can be passed to both in Query
and in Mutation
.
?query={product(id:1){name,info,price}}
In this case, the argument id
for the field product
with a value of 1, says that it is necessary to return the product with the specified identifier.
For list
arguments are omitted, but in a real application it can be, for example: limit
and offset
.
Resolve (Recognizers)
All the logic of working with data (for example, queries to the database, processing and filtering) is in the recognizers, they return the data that will be transmitted to the client as a response to the request.
Type (Type System)
GraphQL uses its type system to describe data. It can be used as base types String
, Int
, Float
, Boolean
, and own (custom). For our example, we need a custom type Product
that will describe all the properties of the product.
var productType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Product",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.Int,
},
"name": &graphql.Field{
Type: graphql.String,
},
"info": &graphql.Field{
Type: graphql.String,
},
"price": &graphql.Field{
Type: graphql.Float,
},
},
},
)
For each field specified base type, in this case graphql.Int
, graphql.String
, graphql.Float
.
The number of nested fields is unlimited, so you can implement a graph system of any level.
Mutation
The mutations are these mutable data, which include: add, edit, and delete. In all other respects, mutations are very similar to regular queries: they also take arguments Args
and return data Resolve
as an answer to a query.
var mutationType = graphql.NewObject(graphql.ObjectConfig{
Name: "Mutation",
Fields: graphql.Fields{
/* Добавление нового продукта
http://localhost:8080/product?query=mutation+_{create(name:"Tequila",info:"Alcohol",price:99){id,name,info,price}}
*/"create": &graphql.Field{
Type: productType,
Description: "Create new product",
Args: graphql.FieldConfigArgument{
"name": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String), // поле обязательное для заполнения
},
"info": &graphql.ArgumentConfig{
Type: graphql.String, // не обязательное поле
},
"price": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Float),
},
},
Resolve: func(params graphql.ResolveParams)(interface{}, error) {
rand.Seed(time.Now().UnixNano())
product := Product{
ID: int64(rand.Intn(100000)), // генерируем случайный ID
Name: params.Args["name"].(string),
Info: params.Args["info"].(string),
Price: params.Args["price"].(float64),
}
products = append(products, product)
return product, nil
},
},
/* Редактирование продукта по id
http://localhost:8080/product?query=mutation+_{update(id:1,price:195){id,name,info,price}}
*/"update": &graphql.Field{
Type: productType,
Description: "Update product by id",
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Int),
},
"name": &graphql.ArgumentConfig{
Type: graphql.String,
},
"info": &graphql.ArgumentConfig{
Type: graphql.String,
},
"price": &graphql.ArgumentConfig{
Type: graphql.Float,
},
},
Resolve: func(params graphql.ResolveParams)(interface{}, error) {
id, _ := params.Args["id"].(int)
name, nameOk := params.Args["name"].(string)
info, infoOk := params.Args["info"].(string)
price, priceOk := params.Args["price"].(float64)
product := Product{}
for i, p := range products {
// Редактируем информацию о продуктеifint64(id) == p.ID {
if nameOk {
products[i].Name = name
}
if infoOk {
products[i].Info = info
}
if priceOk {
products[i].Price = price
}
product = products[i]
break
}
}
return product, nil
},
},
/* Удаление продукта по id
http://localhost:8080/product?query=mutation+_{delete(id:1){id,name,info,price}}
*/"delete": &graphql.Field{
Type: productType,
Description: "Delete product by id",
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Int),
},
},
Resolve: func(params graphql.ResolveParams)(interface{}, error) {
id, _ := params.Args["id"].(int)
product := Product{}
for i, p := range products {
ifint64(id) == p.ID {
product = products[i]
// Удаляем из списка продуктов
products = append(products[:i], products[i+1:]...)
}
}
return product, nil
},
},
},
})
All by analogy with queryType
. There is only one small type feature graphql.NewNonNull(graphql.Int)
that tells us that this field cannot be empty (like NOT NULL
in MySQL)
Everything. Now we have a simple CRUD API on Go to work with products. We did not use the database for this example, but we looked at how to create a data model and manipulate them with mutations.
Examples
If you downloaded the source through
go get github.com/graphql-go/graphql
just go to the directory with an example
cd examples/crud
and run the application
go run main.go
You can use the following queries:
Get product by IDhttp://localhost:8080/product?query={product(id:1){name,info,price}}
Getting a list of productshttp://localhost:8080/product?query={list{id,name,info,price}}
Adding a new producthttp://localhost:8080/product?query=mutation+_{create(name:"Tequila",info:"Strong alcoholic beverage",price:999){id,name,info,price}}
Product editinghttp://localhost:8080/product?query=mutation+_{update(id:1,price:195){id,name,info,price}}
Product removal by idhttp://localhost:8080/product?query=mutation+_{delete(id:1){id,name,info,price}}
If you are using REST you should pay attention to GraphQL as a possible alternative. Yes, at first glance it seems more difficult, but it is worth starting and in a couple of days you will master this technology. At least it will be useful.