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/graphqlNext, 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 Dothat 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)
Queryserves to read (and read only) data. With the help Querywe 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 Nameand Fields, as well as optional Description(used for documentation).
In turn, the field Fieldsalso contains a required field Typeand optional fields Args, ResolveandDescription
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 Queryand in Mutation.
?query={product(id:1){name,info,price}}In this case, the argument idfor the field productwith a value of 1, says that it is necessary to return the product with the specified identifier.
For listarguments are omitted, but in a real application it can be, for example: limitand 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 Productthat 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 Argsand return data Resolveas 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 NULLin 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/graphqljust go to the directory with an example
cd examples/crudand run the application
go run main.goYou 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.