Creating CLI Apps with Go

Learn how to create powerful and efficient CLI apps with Go using the Cobra library. This step-by-step guide demonstrates creating a JSON schema generator from an API response.


Go, with its speed, ease of learning, and extensive standard library, has become a preferred language for creating Command Line Interface (CLI) apps.
In this hands-on guide, we'll show you how to build a simple CLI app named 'schemagen' that can generate JSON schemas from an API response.

Lets get started

Step 1: Setup a new Go project

We begin by creating a new Go project and installing the required dependencies:

1mkdir erratline-code-examples/create-cli-apps
2cd erratline-code-examples/create-cli-apps
3go mod init github.com/bay0/erratline-code-examples/create-cli-apps
4go get github.com/spf13/cobra
5

Step 2: Use cobra-cli

Next, we'll use cobra-cli to create the basic structure of our CLI app:

1go install github.com/spf13/cobra-cli@latest
2cobra-cli init
3

Take a moment to explore the cobra-cli documentation to understand its various features, including automatic integration with Viper.

Your file structure should now resemble:

1.
2├── LICENSE
3├── cmd
4│   └── root.go
5├── go.mod
6├── go.sum
7└── main.go
8

Step 3: Modify the entrypoint

We'll start by modifying './cmd/root.go', setting the command name as schemagen and adding a description:

1var rootCmd = &cobra.Command{
2	Use:   "schemagen",
3	Short: "Generate JSON Schema from API response",
4	Long:  \`Generate JSON Schema from API response for use in validating API responses.\`,
5	Run: func(cmd *cobra.Command, args []string) {
6    fmt.Println("Hello World")
7  },
8},
9

Now we can run our app with go run main.go and we should see the output Hello World.

1$ go run main.go
2Hello World
3

Step 4: Add Flags

We'll add flags for the URL and an optional output file.
As you can see that the url flag is required by adding rootCmd.MarkFlagRequired("url") to the init() function.\

1var (
2  urlFlag string
3  outFlag string
4)
5
6
7func init() {
8	rootCmd.Flags().StringVarP(&urlFlag, "url", "u", "", "URL to the API endpoint")
9	rootCmd.MarkFlagRequired("url") // Mark URL flag as required
10
11	rootCmd.Flags().StringVarP(&outFlag, "out", "o", "", "Output file for the generated JSON Schema")
12	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
13}
14

Step 5: Add utility functions for HTTP requests and writing to files

Lets add our http request function so that we can use it in the Run() function.

1func httpRequest(url string) (string, error) {
2	response, err := http.Get(url)
3	if err != nil {
4		return "", err
5	}
6	defer response.Body.Close()
7
8	body, err := io.ReadAll(response.Body)
9	if err != nil {
10		return "", err
11	}
12
13	return string(body), nil
14}
15
16func writeToFile(filename, content string) error {
17	file, err := os.Create(filename)
18	if err != nil {
19		return err
20	}
21	defer file.Close()
22
23	_, err = file.WriteString(content)
24	return err
25}
26

Now we can use the httpRequest() function in the Run() function to make a request to the url flag.

1Run: func(cmd *cobra.Command, args []string) {
2  if urlFlag == "" {
3    fmt.Println("URL is required")
4    os.Exit(1)
5  }
6
7  response, err := httpRequest(urlFlag)
8  if err != nil {
9    fmt.Println(err)
10    os.Exit(1)
11  }
12
13  fmt.Println(response)
14}
15

Run the app with 'go run main.go --url https://jsonplaceholder.typicode.com/todos/1', and you'll see the output:

1$ go run main.go --url https://jsonplaceholder.typicode.com/todos/1
2{
3  "userId": 1,
4  "id": 1,
5  "title": "delectus aut autem",
6  "completed": false
7}
8

So far so good.
Now we want to add the functionality to generate the json schema from the response.
I wont go into detail on how to generate the json schema.
But I will show you how to add the functionality to our cli app.

1Run: func(cmd *cobra.Command, args []string) {
2		if urlFlag == "" {
3			fmt.Println("URL is required")
4			os.Exit(1)
5		}
6
7		response, err := httpRequest(urlFlag)
8		if err != nil {
9			fmt.Println(err)
10			os.Exit(1)
11		}
12
13		jsonParsed, err := gabs.ParseJSON([]byte(response))
14		if err != nil {
15			fmt.Println("Error parsing JSON:", err)
16			os.Exit(1)
17		}
18
19		schema := generateSchema(jsonParsed)
20
21		// Print the generated schema
22		fmt.Println(schema)
23
24		if outFlag != "" {
25			// Write schema to the output file
26			err := writeToFile(outFlag, schema)
27			if err != nil {
28				fmt.Println("Error writing to file:", err)
29				os.Exit(1)
30			}
31		}
32	},
33

Now we can run our app with go run main.go --url https://jsonplaceholder.typicode.com/todos/1 and we should see the output.

1{
2  "$schema": "http://json-schema.org/draft-07/schema#",
3  "properties": {
4    "completed": {
5      "type": "boolean"
6    },
7    "id": {
8      "type": "number"
9    },
10    "title": {
11      "type": "string"
12    },
13    "userId": {
14      "type": "number"
15    }
16  },
17  "required": [
18    "completed",
19    "userId",
20    "id",
21    "title"
22  ],
23  "title": "Generated schema for Root",
24  "type": "object"
25}
26

Adding tests

Now we want to add some tests to our cli app.
Lets create a new file ./cmd/root_test.go and add the following code.

1package cmd
2
3import (
4	"bytes"
5	"io"
6	"os"
7	"testing"
8
9	"github.com/Jeffail/gabs"
10	"github.com/stretchr/testify/assert"
11)
12
13func TestHttpRequest(t *testing.T) {
14	t.Run("Test http request", func(t *testing.T) {
15		response, err := httpRequest("https://jsonplaceholder.typicode.com/todos/1")
16		assert.NoError(t, err)
17		assert.NotEmpty(t, response)
18	})
19}
20
21func TestWriteToFile(t *testing.T) {
22	t.Run("Test write to file", func(t *testing.T) {
23		fileName := "test.txt"
24		content := "test content"
25
26		err := writeToFile(fileName, content)
27		assert.NoError(t, err)
28
29		file, err := os.Open(fileName)
30		assert.NoError(t, err)
31
32		buf := new(bytes.Buffer)
33		_, err = io.Copy(buf, file)
34		assert.NoError(t, err)
35
36		assert.Equal(t, content, buf.String())
37
38		err = os.Remove(fileName)
39		assert.NoError(t, err)
40	})
41}
42
43func TestGenerateSchema(t *testing.T) {
44	t.Run("Test generate schema", func(t *testing.T) {
45		response, err := httpRequest("https://jsonplaceholder.typicode.com/todos/1")
46		assert.NoError(t, err)
47
48		jsonParsed, err := gabs.ParseJSON([]byte(response))
49		assert.NoError(t, err)
50
51		schema := generateSchema(jsonParsed)
52		assert.NotEmpty(t, schema)
53	})
54}
55
56func TestRootCmd(t *testing.T) {
57	t.Run("Test root command", func(t *testing.T) {
58		rootCmd.SetArgs([]string{"--url", "https://jsonplaceholder.typicode.com/todos/1"})
59		err := rootCmd.Execute()
60		assert.NoError(t, err)
61	})
62}
63
64
65

Now we can run our tests with go test ./... and we should see the output.

1$ go test ./...
2?       github.com/bay0/erratline-code-examples/create-cli-apps [no test files]
3ok      github.com/bay0/erratline-code-examples/create-cli-apps/cmd     0.432s
4

schemagen

Crafting a CLI application with Go doesn't have to be a daunting task.
In this tutorial, we've taken an in-depth look at how to create an functional command-line application.
From the basic setup to making HTTP requests, writing to files, generating JSON schemas, and even ensuring code quality with tests, we've covered the essentials. But what we've built here is just the beginning. With the foundational knowledge you've gained, the sky's the limit.
You can expand on this basic framework to create tools tailored to your specific needs or solve unique challenges in your workflow.
The Go language, combined with libraries like Cobra, makes it incredibly efficient to develop powerful CLI tools.

So, don't hesitate to dive in and start building.
Whether you're creating something for personal use or developing a tool that could benefit the broader community, the skills you've learned today are your gateway to innovation.

Happy coding!

Full code: schemagen