Client patterns
Clients group the SDK API surface into domain-specific bundles. For a step-by-step walkthrough of creating a client from scratch and wiring it into the application interface, see Adding new functionality.
FromClient and dependency injection
Every client struct declares its dependencies as fields and derives FromClient to have them
automatically populated from the SDK Client. The macro generates a from_client method that
extracts each field using the FromClientPart trait — client structs never call Client methods
directly to obtain their dependencies.
#[derive(FromClient)]
pub struct FoldersClient {
pub(crate) key_store: KeyStore<KeySlotIds>,
pub(crate) api_configurations: Arc<ApiConfigurations>,
pub(crate) repository: Option<Arc<dyn Repository<Folder>>>,
}
Some of the available dependency types that can be extracted are:
KeyStore<KeySlotIds>— access to the cryptographic key storeArc<ApiConfigurations>— HTTP API client configurationOption<Arc<dyn Repository<T>>>— state repository for a given domain type
This design also makes clients straightforward to test: because dependencies are plain struct
fields, tests can construct clients directly with test doubles instead of spinning up a full SDK
Client. See Testing below.
WASM support
If the client will be exposed over WASM, annotate both the struct and its impl blocks with:
#[cfg_attr(feature = "wasm", wasm_bindgen)]
UniFFI wrappers
Mobile clients access the SDK through thin wrapper structs in the bitwarden-uniffi crate. Each
wrapper holds a SharedClient (a type alias for Arc<Client>) and delegates to the underlying Rust
client:
pub struct FoldersClient(pub(crate) SharedClient);
#[uniffi::export]
impl FoldersClient {
pub async fn get(&self, folder_id: FolderId) -> Result<FolderView> {
Ok(self.0.vault().folders().get(folder_id).await?)
}
}
The wrapper mirrors the structure of the Rust client hierarchy — parent wrappers expose child
wrappers through accessor methods, just like the application interface clients do. For example, a
VaultClient wrapper returns Arc<FoldersClient>.
Error conversion
UniFFI wrappers use a crate-level Result<T> type alias that maps errors to BitwardenError. This
ensures all errors crossing the FFI boundary are converted into a type that UniFFI can serialize for
Kotlin and Swift consumers. Use the ? operator in wrapper methods to automatically convert
domain-specific errors through the From<E> for BitwardenError implementations.
When introducing a new error type, add a variant for it in
bitwarden-uniffi/src/error.rs and implement the From conversion so it can be
propagated with ?.
Extension traits
Feature crates connect to the SDK Client through extension traits. This keeps feature code
decoupled from Client — the trait is defined in the feature crate, not in bitwarden-core.
pub trait VaultClientExt {
fn vault(&self) -> VaultClient;
}
impl VaultClientExt for Client {
fn vault(&self) -> VaultClient {
VaultClient::new(self.clone())
}
}
The application interface (e.g. PasswordManagerClient) imports the extension trait and calls it to
expose the feature to consumers.
File organization
Start with everything in a single file. Split when the file grows past ~500 lines (including tests).
Single file
Define the client struct, its initialization, and all method impl blocks including tests in one
file. This minimizes indirection and keeps related code easy to discover. Prefer this for smaller
domains.
domain_client.rs
├── DomainClient struct definition
└── impl DomainClient { methods and tests }
Per-method files or subdirectories
When the single file becomes unwieldy, keep the client struct in its own file and give each method
its own file. Each file contains the impl DomainClient block for that method, its DTOs, error
types, and its tests.
domain/
├── domain_client.rs # DomainClient struct definition and initialization
├── mod.rs
├── method_name.rs # impl DomainClient { fn method_name() } + tests
└── other_method.rs # impl DomainClient { fn other_method() } + tests
If a method needs many or large supporting types (request/response structs, error enums), promote it to a subdirectory:
domain/
├── domain_client.rs # DomainClient struct definition and initialization
├── mod.rs
└── method_name/
├── mod.rs # impl DomainClient { fn method_name() } + tests
└── request.rs # supporting types
Do not delegate method bodies to free functions. This splits the implementation away from the API surface, makes the client harder to navigate, and obscures what the method actually does in generated documentation.
impl LoginClient {
// Bad — the real logic lives somewhere else.
pub async fn login_with_password(&self, data: LoginData) -> Result<()> {
login_with_password(self.client, data).await
}
}
Instead, implement the logic directly in the method body.
Testing
Because client structs declare their dependencies as fields, they can be constructed directly in
tests without spinning up a full SDK Client. Inject test doubles for each dependency to isolate
the code under test.
fn create_test_client() -> FoldersClient {
let key_store =
create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
let repository = Arc::new(MemoryRepository::<Folder>::default());
FoldersClient {
key_store,
api_configurations: Arc::new(ApiConfigurations::from_api_client(
ApiClient::new_mocked(|_| {}),
)),
repository: Some(repository),
}
}
Key points:
- Key store — use
create_test_crypto_with_user_keyto set up a key store with a test key. - API configurations — use
ApiClient::new_mockedto create a mock HTTP client. The closure receives requests and can return custom responses. - Repositories — use
MemoryRepositoryas an in-memory test double for state repositories. Populate it with test data before exercising the client method.
Writing test cases
Each public client method should have tests that cover the expected behavior and important error paths. Construct the client, set up any required state, call the method, and assert the result.
#[tokio::test]
async fn test_get_folder() {
let client = create_test_client();
let folder_id = FolderId::new(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"));
// Populate the repository with test data
let folder = client.key_store.encrypt(FolderView {
id: Some(folder_id),
name: "Test Folder".to_string(),
revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
}).unwrap();
client.repository.as_ref().unwrap()
.set(folder_id, folder).await.unwrap();
// Exercise the method and verify
let result = client.get(folder_id).await.unwrap();
assert_eq!(result.name, "Test Folder");
}
#[tokio::test]
async fn test_get_folder_not_found() {
let client = create_test_client();
let folder_id = FolderId::new(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"));
let result = client.get(folder_id).await;
assert!(matches!(result.unwrap_err(), GetFolderError::ItemNotFound(_)));
}