---
title: "Create a Server Driven CLI from your REST API"
description: "Learn how to create a fully server-driven Go CLI from your REST API using OpenAPI and Climate."
canonicalUrl: "https://zuplo.com/blog/2025/02/02/generate-cli-from-api-with-climate"
pageType: "blog"
date: "2025-02-02"
authors: "rahul"
tags: "Go, API Tooling, Tutorial"
image: "https://zuplo.com/og?text=Generate%20a%20CLI%20from%20your%20REST%20API"
---
> This article is written by [Rahul Dé](https://github.com/lispyclouds), a VP of
> Site Reliability Engineering at Citi and creator/maintainer of popular tools
> like [babashka](https://github.com/babashka/babashka),
> [bob](https://github.com/bob-cd/bob), and now
> [climate](https://github.com/lispyclouds/climate). All opinions expressed are
> his own.

APIs, specifically the REST APIs are everywhere and the
[OpenAPI](https://swagger.io/specification/) is pretty much a standard.
Accessing them via various means is a fairly regular thing that a lot of us do
often and when it comes to the CLI (Command Line Interface) languages like Go,
Rust etc are quite popular choices when building. These languages are mostly of
statically typed in nature, favouring a closed world approach of knowing all the
types and paths at compile time to be able to produce lean and efficient binary
for ease of deployment and use.

Like with every engineering choice, there are trade-offs. The one that's here is
the loss of dynamism, namely we see a lot of bespoke tooling in these languages
doing fundamentally the same thing: make HTTP calls and let users have a better
experience than making those calls themselves. The need to know all the types
and paths beforehand causes these perceived maintenance issues:

- Spec duplication: the paths, the schemas etc need to be replicated on the
  client side again. eg when using the popular [Cobra](https://cobra.dev/) lib
  for Go, one must tell it all the possible types beforehand.
- Tighter coupling of client and server: As we have to know each of the paths
  and the methods that a server expects, we need to essentially replicate that
  same thing when making the requests making a tight coupling which is
  susceptible to breakage when the API changes. API is a product having its own
  versioning. eg kubectl only supports
  [certain versions](https://kubernetes.io/releases/version-skew-policy/) of
  kubernetes. Similarly podman or docker CLIs.
- Servers can't influence the client: Ironically to the previous point, as we
  now have replicated the server spec on the client side we effectively have a
  split brain: changes on the server like needing a new parameter etc need to be
  copied over to the client.

All of this put together increases the maintenance overhead and its specially
true for complex tooling like kubectl.

## Using standards

I work primarily on the infra side of this, namely Platform and Site Reliability
Engineering which involves me having other developers as my users and this
cascading effect of an API breakage is quite painful. There are way to work
around this issue and from my experience, being
[spec-first](https://www.atlassian.com/blog/technology/spec-first-api-development)
seems to offer the best balance of development and maintenance velocities.

I am quite a big fan of being spec-first, mainly for the following reasons:

- The API spec is the single source of truth: This is what your users see and
  not your code. Make this the first class citizen like your users and the code
  should use this and not the other way round.
- This keeps all the servers and clients in sync automatically with less
  breakage.
- This keeps a nice separation between the business logic (the API handler code)
  and the infra thereby allowing developers to focus on what's important.

Another project of mine [Bob](https://bob-cd.github.io/) can be seen as an
example of spec-first design. All its tooling follow that idea and its
[CLI](https://bob-cd.github.io/cli/) inspired Climate. A lot of Bob uses
[Clojure](https://clojure.org/) a language that I cherish and who's ideas make
me think better in every other place too.

### Codegen

Although codegen is one of the ways to be spec-first, I personally don't
subscribe to the approach of generating code:

- Introduces another build step adding complexity and more layers of debugging.
- Makes the build more fragile in keeping up with tooling and language changes.
- The generated code comes with its own opinions and is often harder to
  change/mould to our needs.
- It is static code at the end, can't do much at runtime.

## Prior art

- [restish](https://rest.sh/): Inspired some of the ideas behind this. This is a
  project with different goals of being a fully automatic CLI for an OpenAPI
  REST API and is a bit hard to use as a lib.
- [navi](https://github.com/lispyclouds/navi): Server side spec-first library I
  wrote for Clojure which inspired the handler mechanism in Climate.

## What is Climate?

Keeping all of the above into consideration and the fact that Go is one of the
most widely used CLI languages,
[Climate](https://github.com/lispyclouds/climate) was built to address the
issues.

As the name implies, its your mate or sidekick when building CLIs in Go with the
intentions of:

- Keeping the REST API boilerplate away from you.
- Keep the CLI code always in sync with the changes on the server.
- Ability to bootstrap at runtime without any code changes.
- Decoupling you from API machinery, allowing you to focus on just the handlers,
  business logic and things that may not the part of the server calls.
- It does just enough to take the machinery out and not more like making the
  calls for you too; that's business logic.

### How does it work?

Every OpenAPI3 Schema consists of one or more
[Operations](https://swagger.io/docs/specification/v3_0/paths-and-operations/#operations)
having an `OperationId`. An Operation is a combination of the HTTP path, the
method and some parameters.

Overall, Climate works by with these operations at its core. It:

- Parses these from the YAML or JSON file.
- Transforms each of these into a corresponding Cobra command by looking at
  hints from the server.
- Transform each of the parameters into a Flag with the type.
- Build a grouped Cobra command tree and attach it to the root command.

### Servers influencing the CLI

Climate allows the server to influence the CLI behaviour by using OpenAPI's
[extensions](https://swagger.io/docs/specification/v3_0/openapi-extensions/).
This is the secret of Climate's dynamism. Influenced by some of the ideas behind
[restish](https://rest.sh/) it uses the following extensions as of now:

- `x-cli-aliases`: A list of strings which would be used as the alternate names
  for an operation.
- `x-cli-group`: A string to allow grouping subcommands together. All operations
  in the same group would become subcommands in that group name.
- `x-cli-hidden`: A boolean to hide the operation from the CLI menu. Same
  behaviour as a cobra command hide: it's present and expects a handler.
- `x-cli-ignored`: A boolean to tell climate to omit the operation completely.
- `x-cli-name`: A string to specify a different name. Applies to operations and
  request bodies as of now.

### Type checking

As of now, only the primitive types are supported:

- boolean
- integer
- number
- string

More support for
[types](https://swagger.io/docs/specification/v3_0/data-models/data-types/) like
collections and composite types are planned. These are subject to limitations of
what Cobra can do out of the box and what makes sense from a CLI perspective.
There are sensible default behaviour like for request bodies its implicity
`string` which handles most cases. These types are converted to Flags with the
appropriate type checking functions and correctly coerced or the errors reported
when invoked.

Checkout [Wendy](https://github.com/bob-cd/wendy) as a proper example of a
project built with Climate.

### Usage

This assumes an installation of [Go 1.23+](https://go.dev/doc/install) is
available.

```bash
go get github.com/lispyclouds/climate
```

Given a spec:

```yaml
openapi: "3.0.0"

info:
  title: My calculator
  version: "0.1.0"
  description: My awesome calc!

paths:
  "/add/{n1}/{n2}":
    get:
      operationId: AddGet
      summary: Adds two numbers
      x-cli-name: add-get
      x-cli-group: ops
      x-cli-aliases:
        - ag

      parameters:
        - name: n1
          required: true
          in: path
          description: The first number
          schema:
            type: integer
        - name: n2
          required: true
          in: path
          description: The second number
          schema:
            type: integer
    post:
      operationId: AddPost
      summary: Adds two numbers via POST
      x-cli-name: add-post
      x-cli-group: ops
      x-cli-aliases:
        - ap

      requestBody:
        description: The numbers map
        required: true
        x-cli-name: nmap
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NumbersMap"
  "/health":
    get:
      operationId: HealthCheck
      summary: Returns Ok if all is well
      x-cli-name: ping
  "/meta":
    get:
      operationId: GetMeta
      summary: Returns meta
      x-cli-ignored: true
  "/info":
    get:
      operationId: GetInfo
      summary: Returns info
      x-cli-group: info

      parameters:
        - name: p1
          required: true
          in: path
          description: The first param
          schema:
            type: integer
        - name: p2
          required: true
          in: query
          description: The second param
          schema:
            type: string
        - name: p3
          required: true
          in: header
          description: The third param
          schema:
            type: number
        - name: p4
          required: true
          in: cookie
          description: The fourth param
          schema:
            type: boolean

      requestBody:
        description: The requestBody
        required: true
        x-cli-name: req-body

components:
  schemas:
    NumbersMap:
      type: object
      required:
        - n1
        - n2
      properties:
        n1:
          type: integer
          description: The first number
        n2:
          type: integer
          description: The second number
```

Load the spec:

```go
model, err := climate.LoadFileV3("api.yaml") // or climate.LoadV3 with []byte
```

Define a cobra root command:

```go
rootCmd := &cobra.Command{
	Use:   "calc",
	Short: "My Calc",
	Long:  "My Calc powered by OpenAPI",
}
```

#### Handlers and Handler Data:

Define one or more handler functions of the following signature:

```go
func handler(opts *cobra.Command, args []string, data climate.HandlerData) error {
	slog.Info("called!", "data", fmt.Sprintf("%+v", data))
	err := doSomethingUseful(data)

	return err
}
```

**Handler Data**

As of now, each handler is called with the cobra command it was invoked with,
the args and an extra `climate.HandlerData`, more info
[here](https://pkg.go.dev/github.com/lispyclouds/climate#pkg-types)

This can be used to query the params from the command mostly in a type safe
manner:

```go
// to get all the int path params
for _, param := range data.PathParams {
	if param.Type == climate.Integer {
		value, _ := opts.Flags().GetInt(param.Name)
	}
}
```

Define the handlers for the necessary operations. These map to the `operationId`
field of each operation:

```go
handlers := map[string]Handler{
	"AddGet":      handler,
	"AddPost":     handler,
	"HealthCheck": handler,
	"GetInfo":     handler,
}
```

Bootstrap the root command:

```go
err := climate.BootstrapV3(rootCmd, *model, handlers)
```

Continue adding more commands and/or execute:

```go
// add more commands not from the spec

rootCmd.Execute()
```

Sample output:

```bash
$ go run main.go --help
My Calc powered by OpenAPI

Usage:
  calc [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  info        Operations on info
  ops         Operations on ops
  ping        Returns Ok if all is well

Flags:
  -h, --help   help for calc

Use "calc [command] --help" for more information about a command.

$ go run main.go ops --help
Operations on ops

Usage:
  calc ops [command]

Available Commands:
  add-get     Adds two numbers
  add-post    Adds two numbers via POST

Flags:
  -h, --help   help for ops

Use "calc ops [command] --help" for more information about a command.

$ go run main.go ops add-get --help
Adds two numbers

Usage:
  calc ops add-get [flags]

Aliases:
  add-get, ag

Flags:
  -h, --help     help for add-get
      --n1 int   The first number
      --n2 int   The second number

$ go run main.go ops add-get --n1 1 --n2 foo
Error: invalid argument "foo" for "--n2" flag: strconv.ParseInt: parsing "foo": invalid syntax
Usage:
  calc ops add-get [flags]

Aliases:
  add-get, ag

Flags:
  -h, --help     help for add-get
      --n1 int   The first number
      --n2 int   The second number

$ go run main.go ops add-get --n1 1 --n2 2
2024/12/14 12:53:32 INFO called! data="{Method:get Path:/add/{n1}/{n2}}"
```

## Conclusion

Climate results from my experiences of being at the confluence of many teams
developing various tools and proving the need to keep specifications at the
centre of things. WIth this it hopefully inspires others to adopt such
approaches and with static tooling like Go, its still possible to make flexible
things which keep the users at the forefront.