aepcli’s design decisions #

Background #

In my spare time, I work on aep.dev, an resource-oriented API design specification. We’ve done a lot of work around standarization, like updating guidance on resource-oriented design, fleshing out standard CRUDL methods, and design patterns.

I wanted to really prove out that those patterns and consistency could be used to create powerful clients, so I’ve been working on aepcli: a command-line interface to APIs that can consume AEP-compliant HTTP+JSON APIs.

I’ve had some design musings as I’ve been writing it, and I wanted to expand on some here.

Installing aepcli #

If you’d like to follow along (warning: it is a very early alpha as of 2024-10-19), you can install aepcli yourself.

See the readme for the latest instructions, but as of the moment the installation method is a go install:

go install github.com/aep-dev/aepcli/cmd/aepcli@latest

Overall design #

aepcli is heavily inspired by kubectl - itself a command-line interface that is able to interact with a heterogenous collection of resources (those exposed by the Kubernetes API server). It has been a great reference when designing aepcli.

Consuming an OpenAPI definition #

aepcli does not need to re-invent the wheel and use new API document syntax - The OpenAPI Specification is descriptive enough (with the appropriate extensions) to describe the resources and operations supported.

This is also similar to how kubectl works - reading and caching an OpenAPI definition exposed by the Kubernetes api server. For aepcli, the location of the definition is required, so the path is first positional argument, no matter the command:

aepcli https://bookstore.example.com/openapi.json publishers list

The first argument accepts either a URL, or a local file path: this is helpful in the situation where the API itself does not expose an OpenAPI definition, and you need to write one yourself. Or better yet, generate one with aepc!

Adding a config file #

It’s a little cumbersome to add configuration for aepcli every time. In addition, it doesn’t look particularly elegant to include a URL/filename on every invocation.

Kubectl has the concept of a context, which helps configure it to the appropriate API server. This allows kubectl to have a one-to-many relationship, being able to operate on multiple different api servers.

So what if we could do that with aepcli? aepcli supports a config file, located at $HOME/.config/aepcli/config.toml for Linux. You can write something like this:

[apis.roblox]
openapipath = "openapi/roblox.json" # relative paths are taken from the `$HOME/.config/aepcli` directory.
headers = [
    "x-api-key=${ROBLOX_API_KEY}" # add your api key here.
]

And aepcli will let you easily refer to that API by it’s name:

aepcli roblox users get ${USER_ID}
{
  "path": "users/${USER_ID}",
  "name": "NAME",
  "about": "",
  "locale": "en_us",
  "premium": false,
  "idVerified": false
}

This makes support of a new API very easy - just write the appropriate entry in your config, and you’re done!

This one-to-many CLI opens new use cases that other, bespoke CLIs cannot - like using two APIs together. If you need a user id from an authentication provider, and want to use that to do a lookup in some other service, you can do something like:

USERNAME=$(aepcli auth-service get users foo | jq .username)
aepcli docs-service list documents --user=${USERNAME}

client-side dynamicism over code generation #

This point is more nuanced - but aepcli explicity chose to generate the CLI based on an API description schema, instead of opting toward a code-generated client.

From my time working at cloud companies, a common issue customers encountered was with upgrading clients. Whether it was a command-line interface, Terraform provider or SDK, customers would often have to go through painful upgrades to use new fields exposed in the resources, since those clients were code-generated or hand-written and only knew about the fields that the API exposed at the time it was authored.

There are multiple reasons why, in a worst case scenario, these upgrades would take months:

  • Security review.
  • Other executive approvals.
  • Waiting for a centralized platform team to perform the update, who often have limited bandwidth due to serving requests across the whole company.
  • A change in the client for a separate resource being backwards-incompatible, requiring updating usages of that other resource to upgrade.

These delays are harmful to both sides: inaccessible features for the consumer and lost revenue for the service provider.

aepcli is fully dynamic - to use a new field, you don’t need to update any binaries - you just update the OpenAPI document, which is completely in the control of the consumer. Do you want access to a new field? Just add it to your local copy of the OpenAPI document.

Conclusion #

Although these are some of the big design considerations, there’s dozens of smaller ones that I’m sure I’ll hash over at some point. If you have ideas or suggestions, please share them! File an issue over at aepcli, or reach out to me!