consulx
v1.0.0Consul KV CLI, REPL & Rust client
A pure-HTTP Consul KV toolkit built from scratch with reqwest — an interactive REPL (like redis-cli, but for Consul), a lightweight HTTP-only client library, typed JSON prefix loading, and key/prefix watches via blocking queries. Clone the client into your Actix or Axum app state for dynamic config and feature flags.
cargo add consulx// documentation
consulx docs
Overview
consulx is a modern Rust toolkit for working with Consul KV. It pairs an interactive REPL with a lightweight, HTTP-only client library — everything implemented from scratch with reqwest (rustls-tls, so https:// endpoints work with no native OpenSSL), no SDKs.
- Interactive REPL (like redis-cli, but for Consul) with reedline auto-completion
- Raw KV ops, tree view, key/prefix watches, editor integration
- Typed JSON helpers for config and feature flags
- ACL token (X-Consul-Token) and datacenter (?dc=) support
- The ConsulXClient is Clone (a reqwest::Client is an Arc inside) — drop it into web app state
Installation
[dependencies]
consulx = "1.0.0"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["rustls-tls", "json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"Or install the CLI directly:
cargo install consulx
consulx
# Point at a specific endpoint
CONSUL_HTTP_ADDR=http://127.0.0.1:8500 consulxREPL Commands
Command words are case-insensitive. reedline auto-completes the command keywords (after 2 characters) but not keys or values. Ctrl+C and Ctrl+D exit cleanly.
| Command | Usage | Notes |
|---|---|---|
| get | get <key> | Raw value, or <nil> if absent |
| put | put <key> <value> | Preserves internal spaces; strips one surrounding quote pair |
| del / delete | del <key> | Delete a key (alias: delete) |
| list | list [prefix] | Prefix optional (default = whole KV store) |
| tree | tree [prefix] | ASCII tree; prefix optional |
| get-json | get-json <key> | Pretty-prints JSON; warns + shows raw if not JSON |
| put-json | put-json <key> <json> | Validates + minifies; argument kept verbatim |
| edit | edit <key> | Opens value in $EDITOR (default nano) |
| watch | watch <key> | Watch a single key (Ctrl+C to stop) |
| watch-prefix | watch-prefix <prefix> | Watch all keys under a prefix |
| help / ? | help | Show commands (alias: ?) |
| exit / quit | exit | Leave the REPL |
Client API
ConsulXClient is Clone with public fields http (reqwest::Client), base (URL, trailing slash trimmed), and dc (datacenter). Three constructors:
use consulx::ConsulXClient;
// Explicit URL; also reads CONSUL_HTTP_TOKEN + CONSUL_DATACENTER/CONSUL_DC
let c = ConsulXClient::new("http://127.0.0.1:8500")?;
// Explicit token and/or datacenter
let c = ConsulXClient::with_options("http://127.0.0.1:8500", Some(token), Some("dc1".into()))?;
// From CONSUL_HTTP_ADDR (default http://127.0.0.1:8500)
let c = ConsulXClient::from_env()?;// Raw KV operations
kv_get_raw(key) -> Result<Option<String>> // None on 404
kv_put(key, value) -> Result<()>
kv_delete(key) -> Result<()>
kv_list(prefix) -> Result<Vec<String>> // [] on 404; surfaces 403 (ACL) as error
// Typed JSON helpers
kv_get_json<T: DeserializeOwned>(key) -> Result<Option<T>>
kv_put_json<T: Serialize>(key, &T) -> Result<()>
kv_list_json<T: DeserializeOwned>(prefix) -> Result<Vec<(String, T)>>Typed JSON Prefix Loading
Load many typed JSON configs under a prefix in one call — perfect for feature flags and dynamic config. It lists the prefix, then fetches each key (an N+1 pattern), skipping empty values.
#[derive(Deserialize)]
struct FeatureFlag { enabled: bool }
let flags = consul.kv_list_json::<FeatureFlag>("app/features/").await?;
for (key, flag) in flags {
println!("{key} => {:?}", flag);
}Watches (Blocking Queries)
The library watch methods take the last-seen index and return the new index alongside the value, so you drive the blocking-query loop. Consul is polled with a fixed wait=10s; the new index comes from the X-Consul-Index response header.
// returns (new_index, value)
kv_watch(key: &str, index: Option<u64>) -> Result<(u64, Option<String>)>
// returns (new_index, keys)
kv_watch_prefix(prefix: &str, index: Option<u64>) -> Result<(u64, Vec<String>)>
// typical loop
let mut idx = None;
loop {
let (new_idx, value) = consul.kv_watch("app/config", idx).await?;
if Some(new_idx) != idx { println!("changed: {:?}", value); }
idx = Some(new_idx);
}Actix & Axum Usage
There are no bespoke integration types — you just clone the Clone-able client into your framework state and use it from handlers.
// Actix
let consul = ConsulXClient::from_env()?;
App::new()
.app_data(web::Data::new(consul.clone()))
.route("/flag/{k}", web::get().to(get_flag));
async fn get_flag(consul: web::Data<ConsulXClient>, path: web::Path<String>) -> impl Responder {
let v = consul.kv_get_raw(&path).await.unwrap_or(None);
HttpResponse::Ok().body(v.unwrap_or_else(|| "<nil>".into()))
}HTTP & Environment
All requests target {base}/v1/kv/{key}. Keys are percent-encoded but "/" is preserved as a path separator (keys are hierarchical). A configured datacenter is appended as ?dc= on every request; a token is sent as the X-Consul-Token header.
| Variable | Default | Purpose |
|---|---|---|
| CONSUL_HTTP_ADDR | http://127.0.0.1:8500 | Consul base URL (from_env) |
| CONSUL_HTTP_TOKEN | none | ACL token → X-Consul-Token header |
| CONSUL_DATACENTER | none | Datacenter → ?dc= query param |
| CONSUL_DC | none | Fallback for the datacenter |
| EDITOR | nano | Editor for the edit command |
Ready to try consulx?
cargo add consulx// more