The most recent update to Wrangler, version 1.3.1, introduces important new features for developers building Cloudflare Workers — from built-in deployment environments to first class support for Workers KV. Wrangler is Cloudflare’s first officially supported CLI. Branching into this field of software has been a novel experience for us engineers and product folks on the Cloudflare Workers team.
As part of the 1.3.1 release, the folks on the Workers Developer Experience team dove into the thought process that goes into building out features for a CLI and thinking like users. Because while we wish building a CLI were as easy as our teammate Avery tweeted...
If I were programming a CLI, I would simply design it in a way that is not controversial and works for every type of user.
— avery harnish (@SmoothAsSkippy) August 28, 2019
… it brings design challenges that many of us have never encountered. To overcome these challenges successfully requires deep empathy for users across the entire team, as well as the ability to address ambiguous questions related to how developers write Workers.
Wrangler, meet Workers KV
Our new KV functionality introduced a host of new features, from creating KV namespaces to bulk uploading key-value pairs for use within a Worker. This new functionality primarily consisted of logic for interacting with the Workers KV API, meaning that the technical work under “the hood” was relatively straightforward. Figuring out how to cleanly represent these new features to Wrangler users, however, became the fundamental question of this release.
Designing the invocations for new KV functionality unsurprisingly required multiple iterations, and taught us a lot about usability along the way!
Attempt 1
For our initial pass, the path originally seemed so obvious. (Narrator: It really, really wasn’t). We hypothesized that having Wrangler support familiar commands — like ls and rm — would be a reasonable mapping of familiar command line tools to Workers KV, and ended up with the following set of invocations below:
# creates a new KV Namespace
$ wrangler kv add myNamespace
# sets a string key that doesn't expire
$ wrangler kv set myKey=”someStringValue”
# sets many keys
$ wrangler kv set myKey=”someStringValue” myKey2=”someStringValue2” ...
# sets a volatile (expiring) key that expires in 60 s
$ wrangler kv set myVolatileKey=path/to/value --ttl 60s
# deletes three keys
$ wrangler kv rm myNamespace myKey1 myKey2 myKey3
# lists all your namespaces
$ wrangler kv ls
# lists all the keys for a namespace
$ wrangler kv ls myNamespace
# removes all keys from a namespace, then removes the namespace
$ wrangler kv rm -r myNamespace
While these commands invoked familiar shell utilities, they made interacting with your KV namespace a lot more like interacting with a filesystem than a key value store. The juxtaposition of a well-known command like ls
with a non-command, set
, was confusing. Additionally, mapping preexisting command line tools to KV actions was not a good 1-1 mapping (especially for rm -r
; there is no need to recursively delete a KV namespace like a directory if you can just delete the namespace!)
This draft also surfaced use cases we needed to support: namely, we needed support for actions like easy bulk uploads from a file. This draft required users to enter every KV pair in the command line instead of reading from a file of key-value pairs; this was also a non-starter.
Finally, these KV subcommands caused confusion about actions to different resources. For example, the command for listing your Workers KV namespaces looked a lot like the command for listing keys within a namespace.
Going forward, we needed to meet these newly identified needs.
Attempt 2
Our next attempt shed the shell utilities in favor of simple, declarative subcommands like create
, list
, and delete
. It also addressed the need for easy-to-use bulk uploads by allowing users to pass a JSON file of keys and values to Wrangler.
# create a namespace
$ wrangler kv create namespace <title>
# delete a namespace
$ wrangler kv delete namespace <namespace-id>
# list namespaces
$ wrangler kv list namespace
# write key-value pairs to a namespace, with an optional expiration flag
$ wrangler kv write key <namespace-id> <key> <value> --ttl 60s
# delete a key from a namespace
$ wrangler kv delete key <namespace-id> <key>
# list all keys in a namespace
$ wrangler kv list key <namespace-id>
# write bulk kv pairs. can be json file or directory; if dir keys will be file paths from root, value will be contents of files
$ wrangler kv write bulk ./path/to/assets
# delete bulk pairs; same input functionality as above
$ wrangler kv delete bulk ./path/to/assets
Given the breadth of new functionality we planned to introduce, we also built out a taxonomy of new subcommands to ensure that invocations for different resources — namespaces, keys, and bulk sets of key-value pairs — were consistent:
Designing invocations with taxonomies became a crucial part of our development process going forward, and gave us a clear look at the “big picture” of our new KV features.
This approach was closer to what we wanted. It offered bulk put and bulk delete operations that would read multiple key-value pairs from a JSON file. After specifying an action subcommand (e.g. delete
), users now explicitly stated which resource an action applied to (namespace
, key
, bulk
) and reduced confusion about which action applied to which KV component.
This draft, however, was still not as explicit as we wanted it to be. The distinction between operations on namespaces
versus keys
was not as obvious as we wanted, and we still feared the possibility of different delete
operations accidentally producing unwanted deletes (a possibly disastrous outcome!)
Attempt 3
We really wanted to help differentiate where in the hierarchy of structs a user was operating at any given time. Were they operating on namespaces
, keys
, or bulk
sets of keys in a given operation, and how could we make that as clear as possible? We looked around, comparing the ways CLIs from kubectl to Heroku’s handled commands affecting different objects. We landed on a pleasing pattern inspired by Heroku’s CLI: colon-delimited command namespacing:
plugins:install PLUGIN # installs a plugin into the CLI
plugins:link [PATH] # links a local plugin to the CLI for development
plugins:uninstall PLUGIN # uninstalls or unlinks a plugin
plugins:update # updates installed plugins
So we adopted kv:namespace
, kv:key
, and kv:bulk
to semantically separate our commands:
# namespace commands operate on namespaces
$ wrangler kv:namespace create <title> [--env]
$ wrangler kv:namespace delete <binding> [--env]
$ wrangler kv:namespace rename <binding> <new-title> [--env]
$ wrangler kv:namespace list [--env]
# key commands operate on individual keys
$ wrangler kv:key write <binding> <key>=<value> [--env | --ttl | --exp]
$ wrangler kv:key delete <binding> <key> [--env]
$ wrangler kv:key list <binding> [--env]
# bulk commands take a user-generated JSON file as an argument
$ wrangler kv:bulk write <binding> ./path/to/data.json [--env]
$ wrangler kv:bulk delete <binding> ./path/to/data.json [--env]
And ultimately ended up with this topology:
We were even closer to our desired usage pattern; the object acted upon was explicit to users, and the action applied to the object was also clear.
There was one usage issue left. Supplying namespace-id
s--a field that specifies which Workers KV namespace to perform an action to--required users to get their clunky KV namespace-id
(a string like 06779da6940b431db6e566b4846d64db
) and provide it in the command-line under the namespace-id
option. This namespace-id
value is what our Workers KV API expects in requests, but would be cumbersome for users to dig up and provide, let alone frequently use.
The solution we came to takes advantage of the wrangler.toml
present in every Wrangler-generated Worker. To publish a Worker that uses a Workers KV store, the following field is needed in the Worker’s wrangler.toml
:
kv-namespaces = [
{ binding = "TEST_NAMESPACE", id = "06779da6940b431db6e566b4846d64db" }
]
This field specifies a Workers KV namespace that is bound to the name TEST_NAMESPACE
, such that a Worker script can access it with logic like:
TEST_NAMESPACE.get(“my_key”);
We also decided to take advantage of this wrangler.toml
field to allow users to specify a KV binding name instead of a KV namespace id. Upon providing a KV binding name, Wrangler could look up the associated id
in wrangler.toml
and use that for Workers KV API calls.
Wrangler users performing actions on KV namespaces could simply provide --binding TEST_NAMESPACE
for their KV calls let Wrangler retrieve its ID from wrangler.toml
. Users can still specify --namespace-id
directly if they do not have namespaces specified in their wrangler.toml
.
Finally, we reached our happy point: Wrangler’s new KV subcommands were explicit, offered functionality for both individual and bulk actions with Workers KV, and felt ergonomic for Wrangler users to integrate into their day-to-day operations.
Lessons Learned
Throughout this design process, we identified the following takeaways to carry into future Wrangler work:
Taxonomies of your CLI’s subcommands and invocations are a great way to ensure consistency and clarity. CLI users tend to anticipate similar semantics and workflows within a CLI, so visually documenting all paths for the CLI can greatly help with identifying where new work can be consistent with older semantics. Drawing out these taxonomies can also expose missing features that seem like a fundamental part of the “big picture” of a CLI’s functionality.
Use other CLIs for inspiration and validation. Drawing logic from popular CLIs helped us confirm our assumptions about what users like, and learn established patterns for complex CLI invocations.
Avoid logic that requires passing in raw ID strings. Testing CLIs a lot means that remembering and re-pasting ID values gets very tedious very quickly. Emphasizing a set of purely human-readable CLI commands and arguments makes for a far more intuitive experience. When possible, taking advantage of configuration files (like we did with
wrangler.toml
) offers a straightforward way to provide mappings of human-readable names to complex IDs.
We’re excited to continue using these design principles we’ve learned and documented as we grow Wrangler into a one-stop Cloudflare Workers shop.
If you’d like to try out Wrangler, check it out on GitHub and let us know what you think! We would love your feedback.