Use Workers KV directly from Rust
This tutorial will teach you how to read and write to KV directly from Rust, by using wasm_bindgen
and a custom wrapper around the JS KV API.
Before you start
All of the tutorials assume you have already completed the Get started guide, which gets you set up with a Cloudflare Workers account, C3, and Wrangler.
Prerequisites
- Open your terminal and run the
git clone
command to create a basic project using the rustwasm-worker template. cd
into the new project.- Use the current state of the git repository as the initial commit by running the
git add
andgit commit
commands in your terminal.
$ git clone https://github.com/cloudflare/rustwasm-worker-template/ workers-kv-from-rust
$ cd workers-kv-from-rust
$ git add -A
$ git commit -m 'Initial commit'
1. Create and bind a KV namespace
To access KV, you have to define a binding for a particular KV namespace in the wrangler.toml
file generated in your new project’s directory.
If you do not have an existing KV namespace, create one using Wrangler.
For example, a KV namespace called KV_FROM_RUST
would be created by running:
$ wrangler kv:namespace create "KV_FROM_RUST"🌀 Creating namespace with title "workers-kv-from-rust-KV_FROM_RUST"✨ Success!Add the following to your configuration file:kv_namespaces = [ { binding = "KV_FROM_RUST", id = "6257d3ebe5d948cda9e59aae1f9a7f1a" }]
Create a preview ID to use the KV namespace with wrangler preview
:
wrangler kv:namespace create "KV_FROM_RUST" --preview🌀 Creating namespace with title "workers-kv-from-rust-KV_FROM_RUST_preview"✨ Success!Add the following to your configuration file in your kv_namespaces array:{ binding = "KV_FROM_RUST", preview_id = "5c0f32f95cb94819b8c553b470791efd", id = "6257d3ebe5d948cda9e59aae1f9a7f1a" }
Add this binding to the wrangler.toml
file:
wrangler.tomlname = "workers-kv-from-rust"
type = "rust"
account_id = ""
workers_dev = true
route = ""
zone_id = ""
kv_namespaces = [ { binding = "KV_FROM_RUST", preview_id = "5c0f32f95cb94819b8c553b470791efd", id = "6257d3ebe5d948cda9e59aae1f9a7f1a" }
]
2. Pass the KV namespace object to Rust
You can now access this KV namespace as the variable KV_FROM_RUST
in JavaScript. To read or write from the namespace in Rust, you need to pass the whole object to the Rust handler function:
worker/worker.jsaddEventListener('fetch', event => { event.respondWith(handleRequest(event.request));
});
const { handle } = wasm_bindgen;
const instance = wasm_bindgen(wasm);
/** * Fetch and log a request * @param {Request} request */
async function handleRequest(request) { await instance;
return await handle(KV_FROM_RUST, request);
}
The signature of your Rust handler differs from the template, which merely returns a String
from Rust and keeps the request and response handling purely on the JavaScript side.
To pass the request directly to the wasm handler, declare web-sys
as one of your Rust dependencies and explicitly enable the Request
, Response
and ResponseInit
features:
Cargo.toml[dependencies.web-sys]
version = "0.3"
features = [ 'Request', 'Response', 'ResponseInit', 'Url', 'UrlSearchParams',
]
Use Request
and Response
in Rust to create a handler that ignores the request and responds with a 200 OK
status:
src/lib.rsextern crate cfg_if;
extern crate wasm_bindgen;
mod utils;
use cfg_if::cfg_if;
use wasm_bindgen::{JsCast, prelude::*};
use web_sys::{Request, Response, ResponseInit};
cfg_if! { // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. if #[cfg(feature = "wee_alloc")] { extern crate wee_alloc; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; }
}
#[wasm_bindgen]
pub fn handle(kv: JsValue, req: JsValue) -> Result<Response, JsValue> { let req: Request = req.dyn_into()?; let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(None, &init)
}
3. Bind to KV using wasm_bindgen
You are now ready to create a type binding using wasm_bindgen
to access the KV object. Since the KV API returns JavaScript promises, you must first add wasm-bindgen-futures
and js-sys
as dependencies:
Cargo.toml[dependencies]
cfg-if = "0.1.2"
wasm-bindgen = "=0.2.73"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
Add the wrapper and change the type of the kv
argument of your handler accordingly:
src/lib.rs#[wasm_bindgen]
pub fn handle(kv: WorkersKvJs, req: JsValue) -> Result<Response, JsValue> { let req: Request = req.dyn_into()?; let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(None, &init)
}
#[wasm_bindgen]
extern "C" { pub type WorkersKvJs;
#[wasm_bindgen(structural, method, catch)] pub async fn put( this: &WorkersKvJs, k: JsValue, v: JsValue, options: JsValue, ) -> Result<JsValue, JsValue>;
#[wasm_bindgen(structural, method, catch)] pub async fn get( this: &WorkersKvJs, key: JsValue, options: JsValue, ) -> Result<JsValue, JsValue>;
}
4. Create a wrapper around KV
You could start using the kv
parameter as is, but the function signatures generated by wasm_bindgen
can be difficult to work within Rust. For an easier experience, create a struct around the WorkersKvJs
type that wraps it with a more Rust-friendly API:
src/lib.rsuse js_sys::{ArrayBuffer, Object, Reflect, Uint8Array};
struct WorkersKv { kv: WorkersKvJs,
}
impl WorkersKv { async fn put_text(&self, key: &str, value: &str, expiration_ttl: u64) -> Result<(), JsValue> { let options = Object::new(); Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?; self.kv .put(JsValue::from_str(key), value.into(), options.into()) .await?; Ok(()) }
async fn put_vec(&self, key: &str, value: &[u8], expiration_ttl: u64) -> Result<(), JsValue> { let options = Object::new(); Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?; let typed_array = Uint8Array::new_with_length(value.len() as u32); typed_array.copy_from(value); self.kv .put( JsValue::from_str(key), typed_array.buffer().into(), options.into(), ) .await?; Ok(()) }
async fn get_text(&self, key: &str) -> Result<Option<String>, JsValue> { let options = Object::new(); Reflect::set(&options, &"type".into(), &"text".into())?; Ok(self .kv .get(JsValue::from_str(key), options.into()) .await? .as_string()) }
async fn get_vec(&self, key: &str) -> Result<Option<Vec<u8>>, JsValue> { let options = Object::new(); Reflect::set(&options, &"type".into(), &"arrayBuffer".into())?; let value = self.kv.get(JsValue::from_str(key), options.into()).await?; if value.is_null() { Ok(None) } else { let buffer = ArrayBuffer::from(value); let typed_array = Uint8Array::new_with_byte_offset(&buffer, 0); let mut v = vec![0; typed_array.length() as usize]; typed_array.copy_to(v.as_mut_slice()); Ok(Some(v)) } }
}
The above wrapper only exposes a subset of the options supported by the KV API, other options such as expiration
instead of expirationTtl
for PUT
and other types than text
and arrayBuffer
for GET
could be wrapped in a similar fashion. Conceptually, the wrapper methods all manually construct a JavaScript object using Reflect::set
and then convert the return value into a standard Rust type where necessary.
5. Use the wrapper
The following function is an example handler that writes to KV on a PUT
request, using the URL segments to determine the KV document’s key name and value. For example, sending a PUT
request to /foo?value=bar
will write the "bar"
value to the foo
key.
Additionally, the example handler will read from KV when on GET
requests, using the URL pathname as the key name. For example, a GET /foo
request will return the foo
key’s value, if any.
The finalized handle
function:
src/lib.rs#[wasm_bindgen]
pub async fn handle(kv: WorkersKvJs, req: JsValue) -> Result<Response, JsValue> { let req: Request = req.dyn_into()?; let url = web_sys::Url::new(&req.url())?; let pathname = url.pathname(); let query_params = url.search_params(); let kv = WorkersKv { kv }; match req.method().as_str() { "GET" => { let value = kv.get_text(&pathname).await?.unwrap_or_default(); let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(Some(&format!("\"{}\"\n", value)), &init) }, "PUT" => { let value = query_params.get("value").unwrap_or_default(); // set a TTL of 60 seconds: kv.put_text(&pathname, &value, 60).await?; let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(None, &init) }, _ => { let mut init = ResponseInit::new(); init.status(400); Response::new_with_opt_str_and_init(None, &init) } }
}
You can use wrangler dev
to test the Worker:
$ curl 'localhost:8787/foo'""$ curl -X PUT 'localhost:8787/foo?value=bar'
$ curl 'localhost:8787/foo'"bar"
Complete lib.rs
Your lib.rs
should look as follows:
src/lib.rsextern crate cfg_if;
extern crate wasm_bindgen;
mod utils;
use cfg_if::cfg_if;
use js_sys::{ArrayBuffer, Object, Reflect, Uint8Array};
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{Request, Response, ResponseInit};
cfg_if! { // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. if #[cfg(feature = "wee_alloc")] { extern crate wee_alloc; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; }
}
#[wasm_bindgen]
pub async fn handle(kv: WorkersKvJs, req: JsValue) -> Result<Response, JsValue> { let req: Request = req.dyn_into()?; let url = web_sys::Url::new(&req.url())?; let pathname = url.pathname(); let query_params = url.search_params(); let kv = WorkersKv { kv }; match req.method().as_str() { "GET" => { let value = kv.get_text(&pathname).await?.unwrap_or_default(); let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(Some(&format!("\"{}\"\n", value)), &init) } "PUT" => { let value = query_params.get("value").unwrap_or_default(); // set a TTL of 60 seconds: kv.put_text(&pathname, &value, 60).await?; let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(None, &init) } _ => { let mut init = ResponseInit::new(); init.status(400); Response::new_with_opt_str_and_init(None, &init) } }
}
#[wasm_bindgen]
extern "C" { pub type WorkersKvJs;
#[wasm_bindgen(structural, method, catch)] pub async fn put( this: &WorkersKvJs, k: JsValue, v: JsValue, options: JsValue, ) -> Result<JsValue, JsValue>;
#[wasm_bindgen(structural, method, catch)] pub async fn get( this: &WorkersKvJs, key: JsValue, options: JsValue, ) -> Result<JsValue, JsValue>;
}
struct WorkersKv { kv: WorkersKvJs,
}
impl WorkersKv { async fn put_text(&self, key: &str, value: &str, expiration_ttl: u64) -> Result<(), JsValue> { let options = Object::new(); Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?; self.kv .put(JsValue::from_str(key), value.into(), options.into()) .await?; Ok(()) }
async fn put_vec(&self, key: &str, value: &[u8], expiration_ttl: u64) -> Result<(), JsValue> { let options = Object::new(); Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?; let typed_array = Uint8Array::new_with_length(value.len() as u32); typed_array.copy_from(value); self.kv .put( JsValue::from_str(key), typed_array.buffer().into(), options.into(), ) .await?; Ok(()) }
async fn get_text(&self, key: &str) -> Result<Option<String>, JsValue> { let options = Object::new(); Reflect::set(&options, &"type".into(), &"text".into())?; Ok(self .kv .get(JsValue::from_str(key), options.into()) .await? .as_string()) }
async fn get_vec(&self, key: &str) -> Result<Option<Vec<u8>>, JsValue> { let options = Object::new(); Reflect::set(&options, &"type".into(), &"arrayBuffer".into())?; let value = self.kv.get(JsValue::from_str(key), options.into()).await?; if value.is_null() { Ok(None) } else { let buffer = ArrayBuffer::from(value); let typed_array = Uint8Array::new_with_byte_offset(&buffer, 0); let mut v = vec![0; typed_array.length() as usize]; typed_array.copy_to(v.as_mut_slice()); Ok(Some(v)) } }
}
Find the full code for this example on GitHub.