GraphQL API (CRUD) on Go

  • Tutorial

image


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 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,
})

Full code
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.


Let's write mutations for our products.
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/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 ID
http://localhost:8080/product?query={product(id:1){name,info,price}}


Getting a list of products
http://localhost:8080/product?query={list{id,name,info,price}}


Adding a new product
http://localhost:8080/product?query=mutation+_{create(name:"Tequila",info:"Strong alcoholic beverage",price:999){id,name,info,price}}


Product editing
http://localhost:8080/product?query=mutation+_{update(id:1,price:195){id,name,info,price}}


Product removal by id
http://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.


Also popular now: