Skip to content

Using the API from Go ​

This example uses only the Go standard library (net/http and encoding/json), so there is nothing to go get. It needs an API token and the COCOCO_URL and COCOCO_TOKEN environment variables described in Calling the CoCoCo API.

The operations ​

graphql
query ListJobs($first: Int, $after: String) {
  listJobs(first: $first, after: $after) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges { node { id name quantity status dueAt } }
  }
}
graphql
mutation CreateJob($input: CreateJobInput!) {
  createJob(input: $input) {
    job { id name status }
    errors { path message }
  }
}

The program ​

Save this as main.go in a new module (go mod init example.com/jobs). go run . lists every job; go run . create creates one.

go
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

const listJobsQuery = `query ListJobs($first: Int, $after: String) {
  listJobs(first: $first, after: $after) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges { node { id name quantity status dueAt } }
  }
}`

const createJobMutation = `mutation CreateJob($input: CreateJobInput!) {
  createJob(input: $input) {
    job { id name status }
    errors { path message }
  }
}`

type Job struct {
	ID       string  `json:"id"` // a JobID such as "job_01M0SYJHB7FX6BKYVZZ7EAG4Y8"
	Name     string  `json:"name"`
	Quantity int     `json:"quantity"`
	Status   string  `json:"status"`
	DueAt    *string `json:"dueAt"` // ISO 8601, or nil
}

type FieldError struct {
	Path    string `json:"path"`
	Message string `json:"message"`
}

type listJobsData struct {
	ListJobs struct {
		TotalCount *int `json:"totalCount"`
		PageInfo   struct {
			HasNextPage bool    `json:"hasNextPage"`
			EndCursor   *string `json:"endCursor"`
		} `json:"pageInfo"`
		Edges []struct {
			Node Job `json:"node"`
		} `json:"edges"`
	} `json:"listJobs"`
}

type createJobData struct {
	CreateJob struct {
		Job    *Job         `json:"job"`
		Errors []FieldError `json:"errors"`
	} `json:"createJob"`
}

type Client struct {
	Endpoint string
	Token    string
	HTTP     *http.Client
}

// Do posts one operation and decodes its data into out.
// It returns an error for an HTTP error status and for any top-level GraphQL error.
func (c *Client) Do(query string, variables map[string]any, out any) error {
	body, err := json.Marshal(map[string]any{"query": query, "variables": variables})
	if err != nil {
		return err
	}
	req, err := http.NewRequest(http.MethodPost, c.Endpoint, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+c.Token)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.HTTP.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("HTTP %d: %s", resp.StatusCode, msg)
	}

	var result struct {
		Data   json.RawMessage `json:"data"`
		Errors []struct {
			Message string `json:"message"`
		} `json:"errors"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return err
	}
	// Authentication, permission and query errors arrive here with HTTP 200.
	if len(result.Errors) > 0 {
		msgs := make([]string, len(result.Errors))
		for i, e := range result.Errors {
			msgs[i] = e.Message
		}
		return errors.New(strings.Join(msgs, "; "))
	}
	return json.Unmarshal(result.Data, out)
}

// ListJobs returns every job, fetching 100 per request.
func (c *Client) ListJobs() ([]Job, error) {
	var jobs []Job
	var after *string
	for {
		var data listJobsData
		if err := c.Do(listJobsQuery, map[string]any{"first": 100, "after": after}, &data); err != nil {
			return nil, err
		}
		for _, edge := range data.ListJobs.Edges {
			jobs = append(jobs, edge.Node)
		}
		if !data.ListJobs.PageInfo.HasNextPage {
			return jobs, nil
		}
		after = data.ListJobs.PageInfo.EndCursor
	}
}

// CreateJob creates a job. Validation problems come back in the payload.
func (c *Client) CreateJob(name string, quantity int, externalID string) (*Job, error) {
	input := map[string]any{"name": name, "quantity": quantity, "externalId": externalID}
	var data createJobData
	if err := c.Do(createJobMutation, map[string]any{"input": input}, &data); err != nil {
		return nil, err
	}
	if len(data.CreateJob.Errors) > 0 {
		e := data.CreateJob.Errors[0]
		return nil, fmt.Errorf("%s: %s", e.Path, e.Message)
	}
	return data.CreateJob.Job, nil
}

func main() {
	client := &Client{
		Endpoint: strings.TrimRight(os.Getenv("COCOCO_URL"), "/") + "/graphql",
		Token:    os.Getenv("COCOCO_TOKEN"),
		HTTP:     &http.Client{Timeout: 30 * time.Second},
	}

	if len(os.Args) > 1 && os.Args[1] == "create" {
		job, err := client.CreateJob("Business cards", 500, "ERP-10042")
		if err != nil {
			log.Fatal(err)
		}
		fmt.Printf("Created %s (%s)\n", job.ID, job.Status)
		return
	}

	jobs, err := client.ListJobs()
	if err != nil {
		log.Fatal(err)
	}
	for _, job := range jobs {
		fmt.Println(job.ID, job.Status, job.Quantity, job.Name)
	}
}

Notes ​

  • Do returns an error for both kinds of failure: an HTTP error status, and a response whose errors array is not empty.
  • ListJobs follows pageInfo.endCursor until hasNextPage is false. The API returns at most 100 items per page.
  • externalId must be unique in your tenant. Running create twice with the same value returns a validation error in createJob.errors.
  • The code needs Go 1.18 or later (for any).

Connect machines, automate workflows, and build custom apps for print production.