# Rust (Beta) > Source: `src/content/docs/clients/rust.mdx` > Canonical URL: https://rivet.dev/docs/clients/rust > Description: Connect Rust apps to Rivet Actors. --- Rust support is in beta. The supported public Rust API is `rivetkit` and `rivetkit::client`; lower-level crates are internal implementation details and do not carry a stability guarantee. See the full API reference on [docs.rs/rivetkit](https://docs.rs/rivetkit), or the runnable [`hello-world-rust`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-rust) example. ## Getting Started See the [Rust quickstart guide](/docs/actors/quickstart/rust) for getting started. ## Install Add the `rivetkit` crate and its companions: ```sh cargo add rivetkit anyhow async-trait cargo add serde --features derive cargo add tokio --features full ``` The Rust client is strongly typed. It shares the same action and event types as your actor, so define your actor in `src/lib.rs` and import those types from both your server and your client. There is no need to redefine the actor on the client. See [Define Your Actor](/docs/actors/quickstart/rust#define-your-actor) in the quickstart for the actor definition this page builds on. ## Minimal Client ```rust @nocheck use counter::{Counter, Increment}; use rivetkit::{ client::{Client, ClientConfig}, prelude::*, TypedClientExt, }; #[tokio::main] async fn main() -> Result<()> { let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default")); let counter = client.get_or_create_typed_default::("counter", ["my-counter"])?; let count = counter.send(Increment { amount: 1 }).await?; println!("New count: {count}"); Ok(()) } ``` `counter` here is your crate name (the package `name` in `Cargo.toml`, with dashes as underscores). `Counter` and `Increment` are the types you defined alongside your actor. ## Stateless vs Stateful ```rust @nocheck use counter::{Counter, Increment, NewCount}; use rivetkit::{ client::{Client, ClientConfig}, prelude::*, TypedClientExt, }; #[tokio::main] async fn main() -> Result<()> { let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default")); let counter = client.get_or_create_typed_default::("counter", ["my-counter"])?; // Stateless: each call is independent counter.send(Increment { amount: 1 }).await?; // Stateful: keep a connection open for realtime events let connection = counter.connect(); connection .on::(|event| println!("count: {}", event.count)) .await; connection.send(Increment { amount: 1 }).await?; connection.disconnect().await; Ok(()) } ``` A stateless call on the handle opens a short-lived request per action. A connection keeps a WebSocket open so you can receive events and reuse it across calls. ## Getting Actors ```rust @nocheck use counter::Counter; use rivetkit::{ client::{Client, ClientConfig, GetOrCreateOptions}, prelude::*, TypedClientExt, }; use serde_json::json; #[tokio::main] async fn main() -> Result<()> { let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default")); // Get or create an actor let room = client.get_or_create_typed_default::("counter", ["room-42"])?; // Get an existing actor handle (fails when used if the actor does not exist) let existing = client.get_typed_default::("counter", ["room-42"])?; // Create a new actor with input let created = client.get_or_create_typed::( "counter", ["game-1"], GetOrCreateOptions { create_with_input: Some(json!({ "mode": "ranked" })), ..Default::default() }, )?; // Get an actor handle by ID let by_id = client.get_for_id("counter", "actor-id", Default::default())?; // Resolve the actor ID let resolved_id = room.inner().resolve().await?; println!("Resolved ID: {resolved_id}"); Ok(()) } ``` `get_typed_default` / `get_or_create_typed_default` use default options. The non-default variants (`get_typed` / `get_or_create_typed`) take `GetOptions` / `GetOrCreateOptions` for connection parameters, input, and region. ## Connection Parameters Pass connection parameters through the handle options. They are delivered to the actor's `create_conn_state` callback: ```rust @nocheck use counter::Counter; use rivetkit::{ client::{Client, ClientConfig, GetOrCreateOptions}, prelude::*, TypedClientExt, }; use serde_json::json; #[tokio::main] async fn main() -> Result<()> { let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default")); let chat = client.get_or_create_typed::( "counter", ["general"], GetOrCreateOptions { params: Some(json!({ "authToken": "jwt-token-here" })), ..Default::default() }, )?; let connection = chat.connect(); connection.disconnect().await; Ok(()) } ``` ## Subscribing to Events `on` registers a typed callback for an event and returns once the subscription is registered: ```rust @nocheck use counter::{Counter, NewCount}; use rivetkit::{ client::{Client, ClientConfig}, prelude::*, TypedClientExt, }; #[tokio::main] async fn main() -> Result<()> { let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default")); let connection = client .get_or_create_typed_default::("counter", ["general"])? .connect(); connection .on::(|event| println!("count changed: {}", event.count)) .await; Ok(()) } ``` Event callbacks are synchronous and run for every matching event. The actor's emitted event type (here `NewCount`) is decoded into the typed value for you. ## Connection Lifecycle The lower-level connection exposes lifecycle callbacks and the current status. Reach it with `connection.inner()`: ```rust @nocheck use counter::Counter; use rivetkit::{ client::{Client, ClientConfig}, prelude::*, TypedClientExt, }; #[tokio::main] async fn main() -> Result<()> { let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default")); let connection = client .get_or_create_typed_default::("counter", ["general"])? .connect(); let inner = connection.inner().clone(); inner.on_open(|| println!("connected")).await; inner.on_close(|| println!("disconnected")).await; inner.on_error(|message| eprintln!("error: {message}")).await; inner .on_status_change(|status| println!("status: {status:?}")) .await; println!("current status: {:?}", inner.conn_status()); connection.disconnect().await; Ok(()) } ``` `ConnectionStatus` is one of `Idle`, `Connecting`, `Connected`, or `Disconnected`. Connections reconnect automatically with backoff until you call `disconnect`. ## Low-Level HTTP & WebSocket For actors that implement `on_request` or `on_websocket`, call them directly on the untyped handle (`handle.inner()`). `fetch` returns a `reqwest::Response`, and `web_socket` returns a `tokio_tungstenite` stream. This example also needs a few extra crates: ```sh cargo add futures-util tokio-tungstenite cargo add reqwest --features json ``` ```rust @nocheck use counter::Counter; use futures_util::{SinkExt, StreamExt}; use reqwest::{header::HeaderMap, Method}; use rivetkit::{ client::{Client, ClientConfig}, prelude::*, TypedClientExt, }; use tokio_tungstenite::tungstenite::Message; #[tokio::main] async fn main() -> Result<()> { let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default")); let handle = client.get_or_create_typed_default::("counter", ["general"])?; // Raw HTTP request let response = handle .inner() .fetch("history", Method::GET, HeaderMap::new(), None) .await?; let history: Vec = response.json().await?; println!("history: {history:?}"); // Raw WebSocket connection let mut ws = handle.inner().web_socket("stream", None).await?; ws.send(Message::text("hello")).await?; if let Some(message) = ws.next().await { println!("received: {:?}", message?); } Ok(()) } ``` ## Calling from Backend The client is a normal Tokio type, so you can hold it in your backend (Axum, Actix, etc.) and call actors from request handlers. The client is `Clone` and cheap to share: ```rust @nocheck use counter::{Counter, Increment}; use rivetkit::{ client::{Client, ClientConfig}, prelude::*, TypedClientExt, }; async fn increment(client: Client) -> Result { let counter = client.get_or_create_typed_default::("counter", ["server-counter"])?; let count = counter.send(Increment { amount: 1 }).await?; Ok(count) } ``` ## Error Handling Action and connection calls return `anyhow::Result`. Actor-side errors surface as an `anyhow::Error` carrying the error group, code, message, and metadata: ```rust @nocheck use counter::{Counter, Increment}; use rivetkit::{ client::{Client, ClientConfig}, prelude::*, TypedClientExt, }; #[tokio::main] async fn main() -> Result<()> { let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default")); let counter = client.get_or_create_typed_default::("counter", ["my-counter"])?; match counter.send(Increment { amount: 1 }).await { Ok(count) => println!("count: {count}"), Err(error) => eprintln!("action failed: {error:#}"), } Ok(()) } ``` ## Concepts ### Keys Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing: ```rust @nocheck use counter::Counter; use rivetkit::{ client::{Client, ClientConfig}, prelude::*, TypedClientExt, }; #[tokio::main] async fn main() -> Result<()> { let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default")); // Compound key: [org, room] let room = client.get_or_create_typed_default::("counter", ["org-acme", "general"])?; let actor_id = room.inner().resolve().await?; println!("Actor ID: {actor_id}"); Ok(()) } ``` Keys accept arrays of `&str` or `String` (`["org-acme", "general"]`). Don't build keys with string interpolation like `format!("org:{user_id}")` when `user_id` contains user data. Use arrays instead to prevent key injection attacks. ### Configuration `ClientConfig::new(endpoint)` is a builder. The endpoint is always required; there is no default. Common options: ```rust @nocheck use rivetkit::client::ClientConfig; let config = ClientConfig::new("http://localhost:6420") .namespace("default") .token("pk_...") .pool_name("my-pool") .header("x-custom", "value"); ``` - `namespace` - target namespace (defaults to the engine's configured namespace). - `token` - authentication token for the engine. - `pool_name` - runner pool to target. - `header` / `headers` - extra HTTP headers sent with each request. - `max_input_size` - cap on encoded action input size. For serverless deployments, set the endpoint to your app's `/api/rivet` URL. See [Endpoints](/docs/general/endpoints) for details. ## API Reference See the full client API documentation on [docs.rs/rivetkit-client](https://docs.rs/rivetkit-client/latest/rivetkit_client/). _Source doc path: /docs/clients/rust_