Saturday, October 28, 2023

Consuming Shopify Product API Example

In this blog, we are going to learn how to pull product data from the Shopify API. To build upon previous examples like Starting Out on the go-chi Framework and GraphQL with Postman, we will implement this in Go using the go-chi framework and hit the Shopify GraphQL endpoint. Let's begin.

User Story

As a user, I want to retrieve my Shopify products in JSON format, so I can use it in my analytics software.

Shopify Setup

First off, I will not go into too much detail in setting Shopify up. I will let the Shopify documentation do that for me. To get this example running, you'll need a Shopify Partner account. When I created my Shopify partner account, it was free and I don't see any reason that it won't be in the future. Once you have a pertner account, create a development store or use the quickstart store with test data preloaded.

Go into your store, then Settings, then Apps and sales channels, then Develop apps and then Create an app.

On the Configuration tab you must have the access scopes write_products and read_products. As of this writing, the webhook version is 2023-10.

On the API credentials tab, take note of your Admin API access token (you'll need to add this in the request header). Keep your access tokens secure. Only share them with developers that you trust to safely access your data. Take note of your API key and secret key as well.

There you have it. We are all set on the Shopify configuration end.

Testing the Shopify API

Before we go straight into coding, let's test the API call first. Fire up Postman and let's hit the GraphQL endpoint. The URL is of the form https://<storename>.myshopify.com/admin/api/<version>/graphql.json. Add X-Shopify-Access-Token in your header and the value is your Admin API access token. We'll do a query for the first 5 products. The image below shows we can happily make a request. Go to GraphQL Admin API reference for more details.


query {
  products(first: 5, reverse: false) {
    edges {
      node {
        id
        title
        handle
        vendor
        productType
        tags
      }
    }
  }
}

The Main

main.go


// snipped...

func main() {
	config, err := config.Load("./conf/config.json")
	if err != nil {
		panic(err)
	}

	r := chi.NewRouter()
	r.Use(middleware.Logger)

	r.Get("/", router.GetStatus)

	r.Mount("/products", router.GetProducts(config))

	http.ListenAndServe(fmt.Sprintf(":%v", config.Port), r)
}

The first thing the program does is load up the configuration. As mentioned above, we are building upon a previous example, Starting Out on the go-chi Framework. We utilize the go-chi framework for routing incoming HTTP reqeusts. Any requests to the root resource will return a status in JSON format. Any requests to the /products resource will return a list of products in JSON format. I will not expand on router.GetStatus because I have explained it in my previously mentioned blog, Starting Out on the go-chi Framework. I just moved it into a new package making it more organized.

Configuration

config.go


// snipped...

type Config struct {
	Port    int     `json:"port"`
	Shopify Shopify `json:"shopify"`
}

type Shopify struct {
	Endpoint    string `json:"endpoint"`
	AccessToken string `json:"accessToken"`
}

func Load(filePath string) (Config, error) {
	config, err := unmarshalFile[Config](filePath)
	if err != nil {
		return Config{}, err
	}

	return config, nil
}

func unmarshalFile[T any](filePath string) (T, error) {
	pointer := new(T)

	f, err := os.Open(filePath)
	if err != nil {
		return *pointer, fmt.Errorf("opening config file: %w", err)
	}
	defer f.Close()

	jsonBytes, err := io.ReadAll(f)
	if err != nil {
		return *pointer, fmt.Errorf("reading config file: %w", err)
	}

	err = json.Unmarshal(jsonBytes, &pointer)
	if err != nil {
		return *pointer, fmt.Errorf("unmarshalling config file: %w", err)
	}

	return *pointer, nil
}

Making the configuration file in JSON format will make it easy for us to use. We simply read the file and unmarshal the byte array into our Config type and bob's your uncle. Then we easily get the value accessing the fields as seen in main.go(e.g. fmt.Sprintf(":%v", config.Port)).

JSON Marshaller

json.go


func Marshal(v any) []byte {
	jsonBytes, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}

	return jsonBytes
}

func Unmarshal[T any](data []byte) T {
	var result T

	err := json.Unmarshal(data, &result)
	if err != nil {
		panic(err)
	}

	return result
}

Nothing special with this file. It is just a wrapper of the standard JSON Go library. Didn't want to muddy the resource handlers with JSON error handling.

Get Products

products.go


// snipped...
type GqlQuery struct {
	Query string `json:"query"`
}

type GqlResponse struct {
	Data       Data       `json:"data,omitempty"`
	Extensions Extensions `json:"extensions,omitempty"`
}

type Data struct {
	Products Products `json:"products,omitempty"`
}

type Products struct {
	Edges []Edge `json:"edges,omitempty"`
}

type Edge struct {
	Node Node `json:"node,omitempty"`
}

type Node struct {
	Id          string   `json:"id,omitempty"`
	Title       string   `json:"title,omitempty"`
	Handle      string   `json:"handle,omitempty"`
	Vendor      string   `json:"vendor,omitempty"`
	ProductType string   `json:"productType,omitempty"`
	Tags        []string `json:"tags,omitempty"`
}

type Extensions struct {
	Cost Cost `json:"cost,omitempty"`
}

type Cost struct {
	RequestedQueryCost int            `json:"requestedQueryCost,omitempty"`
	ActualQueryCost    int            `json:"actualQueryCost,omitempty"`
	ThrottleStatus     ThrottleStatus `json:"throttleStatus,omitempty"`
}

type ThrottleStatus struct {
	MaximumAvailable   float32 `json:"maximumAvailable,omitempty"`
	CurrentlyAvailable int     `json:"currentlyAvailable,omitempty"`
	RestoreRate        float32 `json:"restoreRate,omitempty"`
}

func GetProducts(config config.Config) chi.Router {
	router := chi.NewRouter()

	router.Get("/", func(w http.ResponseWriter, r *http.Request) {
		productsGql := fmt.Sprintf(`{
			products(first: 3, reverse: false) {
			  edges {
				node {
				  id
				  title
				  handle
				  vendor
				  productType
				  tags
				}
			  }
			}
		  }`)

		query := GqlQuery{
			Query: productsGql,
		}

		q := marshaller.Marshal(query)
		body := strings.NewReader(string(q))

		client := &http.Client{}
		req, err := http.NewRequest(http.MethodPost, config.Shopify.Endpoint, body)
		if err != nil {
			panic(err)
		}
		req.Header.Add("Content-Type", "application/json")
		req.Header.Add("X-Shopify-Access-Token", config.Shopify.AccessToken)

		resp, err := client.Do(req)
		if err != nil {
			panic(err)
		}
		defer resp.Body.Close()

		fmt.Println("Response status:", resp.Status)

		responseBody, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			panic(err)
		}

		gqlResp := marshaller.Unmarshal[GqlResponse](responseBody)

		jsonBytes := marshaller.Marshal(gqlResp.Data.Products.Edges)

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(200)
		w.Write(jsonBytes)
	})

	return router
}

Welcome to the heart of this article. We start by creating the type struct of the GraphQL response so we can easily unmarshal it. As you saw earlier in Testing the Shopify API, the Postman image shows a snapshot of the GraphQL response from Shopify. When GetProducts is called, it starts by creating the GraphQL query for products. Marshal the query into a byte array then passes it into a new HTTP request. We create an HTTP client so we can add in the Shopify Access Token in the header and the content type. We then send the request by calling client.Do(req) and read the response. Converting the response into a byte array so we can unmarshal it. After that we strip out the stuff we don't need. We only need the Nodes. Once we have the nodes, we convert it into bytes and send it back. That is all there is to it. Oh yeah, don't forget to close response after using it.

Output

I've set my port to 4000 so hitting http://localhost:4000 will return a response like below:


{
    "id": 1,
    "message": "API ready and waiting"
}

Hitting http://localhost:4000/products will return a response like below:


[
    {
        "node": {
            "id": "gid://shopify/Product/8787070189860",
            "title": "The Videographer Snowboard",
            "handle": "the-videographer-snowboard",
            "vendor": "Quickstart (5cec88e7)"
        }
    },
    {
        "node": {
            "id": "gid://shopify/Product/8787070222628",
            "title": "The Minimal Snowboard",
            "handle": "the-minimal-snowboard",
            "vendor": "Quickstart (5cec88e7)"
        }
    },
    {
        "node": {
            "id": "gid://shopify/Product/8787070386468",
            "title": "The Archived Snowboard",
            "handle": "the-archived-snowboard",
            "vendor": "Snowboard Vendor",
            "tags": [
                "Archived",
                "Premium",
                "Snow",
                "Snowboard",
                "Sport",
                "Winter"
            ]
        }
    }
]

Consuming Shopify Product API Wrap Up

There you have it. A way to pull data from the Shopify GraphQL Admin API and spitting it out in a slightly different format. In between we utilized the go-chi framework for routing requests. We used the standard Go library for JSON manipulation, HTTP communication and file access. Lastly, did we satisfy the user story? Grab the full repo here, github.com/jpllosa/shopify-product-api.

No comments:

Post a Comment