Skip to content

Migrating from Rust

JavaScript and Rust services are fully interoperable: they run on the same network, sync state through the same CRDTs, and emit/receive the same events. This guide maps the Rust service model to its TypeScript equivalent.

use calimero_sdk::app;
use calimero_storage::collections::UnorderedMap;
#[app::state]
#[derive(BorshSerialize, BorshDeserialize)]
pub struct KvStore {
items: UnorderedMap<String, String>,
}
#[app::logic]
impl KvStore {
#[app::init]
pub fn init() -> KvStore {
KvStore { items: UnorderedMap::new() }
}
pub fn set(&mut self, key: String, value: String) {
self.items.insert(key, value).unwrap();
}
pub fn get(&self, key: &str) -> Option<String> {
self.items.get(key).unwrap()
}
}
import { State, Logic, Init, View, createUnorderedMap } from '@calimero-network/calimero-sdk-js';
import type { UnorderedMap } from '@calimero-network/calimero-sdk-js/collections';
@State
export class KvStore {
// Initialize CRDT fields inline with the factory helpers.
items: UnorderedMap<string, string> = createUnorderedMap();
}
@Logic(KvStore)
export class KvStoreLogic extends KvStore {
@Init
static initialize(): KvStore {
return new KvStore();
}
set(key: string, value: string): void {
this.items.set(key, value);
}
@View()
get(key: string): string | null {
return this.items.get(key);
}
}
Rust TypeScript
#[app::state] @State
#[app::logic] @Logic(StateClass)
#[app::init] @Init
#[app::event] @Event
(read-only fn) @View()

In Rust, state and logic live on one type. In TypeScript they are two classes: a @State data class and a separate @Logic(StateClass) class that holds the callable methods.

Prefer inline field initializers using the create* helpers (items = createUnorderedMap()) over assigning inside a constructor. The logic methods run with this bound to a hydrated state instance; inline defaults make the runtime reattach the persisted collection ID instead of allocating a new one on each call.

@State
export class MyApp {
items: UnorderedMap<string, string> = createUnorderedMap();
count: Counter = createCounter();
}

Rust uses Result<T, E> and ?. In TypeScript, collection operations throw on failure — no explicit Result:

set(key: string, value: string): void {
this.items.set(key, value); // throws on error
}

Rust distinguishes &self from &mut self. In TypeScript, mark read-only entry points with @View() so the dispatcher skips persistence — otherwise the method is assumed to mutate state and a (possibly empty) delta is produced.

Rust TypeScript Notes
UnorderedMap<K, V> UnorderedMap<K, V> JS remove(key) returns void
UnorderedSet<T> UnorderedSet<T> JS removal is delete(value)
Vector<T> Vector<T> len(), push(), pop()
Counter Counter value() returns bigint
LwwRegister<T> LwwRegister<T> timestamp() is number|null

See the Collections guide for full method lists.

// ❌ BAD — field never initialized
@State
export class MyApp {
items: UnorderedMap<string, string>;
}
// ✅ GOOD — inline factory initializer
@State
export class MyApp {
items: UnorderedMap<string, string> = createUnorderedMap();
}
// ❌ BAD — methods belong on @Logic, not @State
@State
export class MyApp {
items: UnorderedMap<string, string> = createUnorderedMap();
set(key: string, value: string) { this.items.set(key, value); }
}
// ✅ GOOD
@State
export class MyApp {
items: UnorderedMap<string, string> = createUnorderedMap();
}
@Logic(MyApp)
export class MyAppLogic extends MyApp {
set(key: string, value: string) { this.items.set(key, value); }
}

Use the Rust SDK when performance is critical, you’re building complex algorithms, or you need the smallest possible WASM (a Rust service is roughly an order of magnitude smaller than the ~500 KB QuickJS-based JS output).

Use the JavaScript SDK for rapid prototyping, when your team knows TypeScript, when you want the npm ecosystem, or when developer experience is the priority.