Ergonomic Rust bindings to Cloudflare Workers environment. Write your entire worker in Rust!
Read the Notes and FAQ
use worker::*;
#[event(fetch)]
pub async fn main(mut req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
console_log!(
"{} {}, located at: {:?}, within: {}",
req.method().to_string(),
req.path(),
req.cf().unwrap().coordinates().unwrap_or_default(),
req.cf().unwrap().region().unwrap_or("unknown region".into())
);
if !matches!(req.method(), Method::Post) {
return Response::error("Method Not Allowed", 405);
}
if let Some(file) = req.form_data().await?.get("file") {
return match file {
FormEntry::File(buf) => {
Response::ok(&format!("size = {}", buf.bytes().await?.len()))
}
_ => Response::error("`file` part of POST form must be a file", 400),
};
}
Response::error("Bad Request", 400)
}
The project uses wrangler for running and publishing your Worker.
Use cargo generate to start from a template:
cargo generate cloudflare/workers-rs
There are several templates to choose from. During generation you will be prompted to enable
panic=unwind and abort recovery (see Panic Recovery
below). You should see a new project layout with a src/lib.rs. Start there! Use any local or
remote crates and modules (as long as they compile to the wasm32-unknown-unknown target).
Once you're ready to run your project, run your worker locally:
npx wrangler dev
Finally, go live:
# configure your routes, zones & more in your worker's `wrangler.toml` file
npx wrangler deploy
If you would like to have wrangler installed on your machine, see instructions in wrangler repository.
http Featureworker 0.0.21 introduced an http feature flag which starts to replace custom types with widely used types from the http crate.
This makes it much easier to use crates which use these standard types such as axum and hyper.
This currently does a few things:
Body, which implements http_body::Body and is a simple wrapper around web_sys::ReadableStream. req argument when using the [event(fetch)] macro becomes http::Request<worker::Body>.http::Response<B> where B can be any http_body::Body<Data=Bytes>.Fetcher::fetch_request is http::Request<worker::Body>. Fetcher::fetch_request is Result<http::Response<worker::Body>>.The end result is being able to use frameworks like axum directly (see example):
pub async fn root() -> &'static str {
"Hello Axum!"
}
fn router() -> Router {
Router::new().route("/", get(root))
}
#[event(fetch)]
async fn fetch(
req: HttpRequest,
_env: Env,
_ctx: Context,
) -> Result<http::Response<axum::body::Body>> {
Ok(router().call(req).await?)
}
We also implement try_from between worker::Request and http::Request<worker::Body>, and between worker::Response and http::Response<worker::Body>. This allows you to convert your code incrementally if it is tightly coupled to the original types.
Router:Parameterize routes and access the parameter values from within a handler. Each handler function takes a
Request, and a RouteContext. The RouteContext has shared data, route params, Env bindings, and more.
use serde::{Deserialize, Serialize};
use worker::*;
#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
// Create an instance of the Router, which can use parameters (/user/:name) or wildcard values
// (/file/*pathname). Alternatively, use `Router::with_data(D)` and pass in arbitrary data for
// routes to access and share using the `ctx.data()` method.
let router = Router::new();
// useful for JSON APIs
#[derive(Deserialize, Serialize)]
struct Account {
id: u64,
// ...
}
router
.get_async("/account/:id", |_req, ctx| async move {
if let Some(id) = ctx.param("id") {
let accounts = ctx.kv("ACCOUNTS")?;
return match accounts.get(id).json::<Account>().await? {
Some(account) => Response::from_json(&account),
None => Response::error("Not found", 404),
};
}
Response::error("Bad Request", 400)
})
// handle files and fields from multipart/form-data requests
.post_async("/upload", |mut req, _ctx| async move {
let form = req.form_data().await?;
if let Some(entry) = form.get("file") {
match entry {
FormEntry::File(file) => {
let bytes = file.bytes().await?;
}
FormEntry::Field(_) => return Response::error("Bad Request", 400),
}
// ...
if let Some(permissions) = form.get("permissions") {
// permissions == "a,b,c,d"
}
// or call `form.get_all("permissions")` if using multiple entries per field
}
Response::error("Bad Request", 400)
})
// read/write binary data
.post_async("/echo-bytes", |mut req, _ctx| async move {
let data = req.bytes().await?;
if data.len() < 1024 {
return Response::error("Bad Request", 400);
}
Response::from_bytes(data)
})
.run(req, env).await
}
All "bindings" to your script (Durable Object & KV Namespaces, Secrets, Variables and Version) are
accessible from the env parameter provided to both the entrypoint (main in this example), and to
the route handler callback (in the ctx argument), if you use the Router from the worker crate.
use worker::*;
#[event(fetch, respond_with_errors)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
let router = Router::new();
router
.on_async("/durable", |_req, ctx| async move {
let namespace = ctx.durable_object("CHATROOM")?;
let stub = namespace.id_from_name("A")?.get_stub()?;
// `fetch_with_str` requires a valid Url to make request to DO. But we can make one up!
stub.fetch_with_str("http://fake_url.com/messages").await
})
.get("/secret", |_req, ctx| {
Response::ok(ctx.secret("CF_API_TOKEN")?.to_string())
})
.get("/var", |_req, ctx| {
Response::ok(ctx.var("BUILD_NUMBER")?.to_string())
})
.post_async("/kv", |_req, ctx| async move {
let kv = ctx.kv("SOME_NAMESPACE")?;
kv.put("key", "value")?.execute().await?;
Response::empty()
})
.run(req, env).await
}
For more information about how to configure these bindings, see:
To define a Durable Object using the worker crate you need to implement the DurableObject trait
on your own struct. Additionally, the #[durable_object] attribute macro must be applied to the struct definition.
use worker::{durable_object, DurableObject, State, Env, Result, Request, Response};
#[durable_object]
pub struct Chatroom {
users: Vec<User>,
messages: Vec<Message>,
state: State,
env: Env, // access `Env` across requests, use inside `fetch`
}
impl DurableObject for Chatroom {
fn new(state: State, env: Env) -> Self {
Self {
users: vec![],
messages: vec![],
state: state,
env,
}
}
async fn fetch(&self, _req: Request) -> Result<Response> {
// do some work when a worker makes a request to this DO
Response::ok(&format!("{} active users.", self.users.len()))
}
}
You'll need to "migrate" your worker script when it's published so that it is aware of this new
Durable Object, and include a binding in your wrangler.toml.
wrangler.toml file:# ...
[durable_objects]
bindings = [
{ name = "CHATROOM", class_name = "Chatroom" } # the `class_name` uses the Rust struct identifier name
]
[[migrations]]
tag = "v1" # Should be unique for each entry
new_classes = ["Chatroom"] # Array of new classes
Durable Objects can use SQLite for persistent storage, providing a relational database interface. To enable SQLite storage, you need to use new_sqlite_classes in your migration and access the SQL storage through state.storage().sql().
use worker::{durable_object, DurableObject, State, Env, Result, Request, Response, SqlStorage};
#[durable_object]
pub struct SqlCounter {
sql: SqlStorage,
}
impl DurableObject for SqlCounter {
fn new(state: State, _env: Env) -> Self {
let sql = state.storage().sql();
// Create table if it does not exist
sql.exec("CREATE TABLE IF NOT EXISTS counter(value INTEGER);", None)
.expect("create table");
Self { sql }
}
async fn fetch(&self, _req: Request) -> Result<Response> {
#[derive(serde::Deserialize)]
struct Row {
value: i32,
}
// Read current value
let rows: Vec<Row> = self
.sql
.exec("SELECT value FROM counter LIMIT 1;", None)?
.to_array()?;
let current = rows.get(0).map(|r| r.value).unwrap_or(0);
let next = current + 1;
// Update counter
self.sql.exec("DELETE FROM counter;", None)?;
self.sql
.exec("INSERT INTO counter(value) VALUES (?);", vec![next.into()])?;
Response::ok(format!("SQL counter is now {}", next))
}
}
Configure your wrangler.toml to enable SQLite storage:
# ...
[durable_objects]
bindings = [
{ name = "SQL_COUNTER", class_name = "SqlCounter" }
]
[[migrations]]
tag = "v1" # Should be unique for each entry
new_sqlite_classes = ["SqlCounter"] # Use new_sqlite_classes for SQLite-enabled objects
As queues are in beta you need to enable the queue feature flag.
Enable it by adding it to the worker dependency in your Cargo.toml:
worker = {version = "...", features = ["queue"]}
use worker::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug, Clone, Deserialize)]
pub struct MyType {
foo: String,
bar: u32,
}
// Consume messages from a queue
#[event(queue)]
pub async fn main(message_batch: MessageBatch<MyType>, env: Env, _ctx: Context) -> Result<()> {
// Get a queue with the binding 'my_queue'
let my_queue = env.queue("my_queue")?;
// Deserialize the message batch
let messages = message_batch.messages()?;
// Loop through the messages
for message in messages {
// Log the message and meta data
console_log!(
"Got message {:?}, with id {} and timestamp: {}",
message.body(),
message.id(),
message.timestamp().to_string()
);
// Send the message body to the other queue
my_queue.send(message.body()).await?;
// Ack individual message
message.ack();
// Retry individual message
message.retry();
}
// Retry all messages
message_batch.retry_all();
// Ack all messages
message_batch.ack_all();
Ok(())
}
You'll need to ensure you have the correct bindings in your wrangler.toml:
# ...
[[queues.consumers]]
queue = "myqueueotherqueue"
max_batch_size = 10
max_batch_timeout = 30
[[queues.producers]]
queue = "myqueue"
binding = "my_queue"
workers-rs has experimental support for Workers RPC.
For now, this relies on JavaScript bindings and may require some manual usage of wasm-bindgen.
Not all features of RPC are supported yet (or have not been tested), including: - Function arguments and return values - Class instances - Stub forwarding
Writing an RPC server with workers-rs is relatively simple. Simply export methods using wasm-bindgen. These
will be automatically detected by worker-build and made available to other Workers. See
example.
Creating types and bindings for invoking another Worker's RPC methods is a bit more involved. You will need to
write more complex wasm-bindgen bindings and some boilerplate to make interacting with the RPC methods more
idiomatic. See [example](./examples/rpc-client/src/calculator.rs
$ claude mcp add workers-rs \
-- python -m otcore.mcp_server <graph>