metaxy
/mɛˈtæk.si/ · Greek: μεταξύ · the in-between
End-to-end typesafe RPC between
Rust lambdas & TS frontend
#[derive(Serialize)]
pub struct User {
pub id: u64,
pub name: String,
pub active: bool,
}// Auto-generated
interface User {
id: number;
name: string;
active: boolean;
}End-to-end type safety
Define a Rust struct once — the CLI generates matching TypeScript interfaces. Rename a field, and
your frontend won't compile until it's updated. No more any, no runtime surprises.
Auto-generated client
A fully typed rpc.query() / rpc.mutate() client with autocomplete for every procedure. Input and output types are
inferred from your Rust code.
const rpc = createRpcClient({ baseUrl: '/api' });
const greeting = await rpc.query('hello', 'World');
// ^ string — fully typed, with autocomplete$ metaxy watch
▸ Watching api/ for changes...
✓ Generated rpc-types.ts (3 queries, 1 mutation)
▸ api/users.rs changed
✓ Re-generated (4 queries, 1 mutation)Watch mode
Save a .rs file and types regenerate instantly. Runs alongside your dev server so the
frontend always has the latest types without manual steps.
Macro-driven
Annotate with #[rpc_query] or #[rpc_mutation] and get CORS, input parsing, JSON serialization, error handling, and
HTTP method validation — all generated at compile time.
#[rpc_query]
async fn hello(name: String) -> String {
format!("Hello, {}!", name)
}#[rpc_query(init = "setup")]
async fn get_users(state: &AppState) -> Vec<User> {
sqlx::query_as("SELECT * FROM users")
.fetch_all(&state.pool).await.unwrap()
}Init & state injection
Set up database pools, HTTP clients, or loggers once at cold start. The macro injects shared state
as &T into your handler automatically.
Serde support
rename_all, skip, flatten, and all four enum tagging strategies — the codegen reads your serde
attributes and generates TypeScript that matches the actual JSON output.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UserProfile {
pub display_name: String,
pub email: String,
#[serde(skip)]
pub password_hash: String,
}#[rpc_query(cache = "1h")]
async fn get_config() -> AppConfig {
// Response cached at Vercel's edge for 1 hour
load_config().await
}Edge caching
Add cache = "1h" and the macro generates Cache-Control headers. On Vercel,
this enables CDN caching with zero infrastructure changes.
Vercel-native
Each .rs file in api/ becomes a serverless function. No routing config, no server setup — just deploy with vercel.
my-app/
├── api/
│ ├── hello.rs → /api/hello
│ └── users.rs → /api/users
├── src/ # frontend (any framework)
├── Cargo.toml
└── vercel.json// Svelte 5
const user = createQuery(rpc, 'get_user', () => id);
// React
const user = useQuery(rpc, 'get_user', id);
// Vue 3
const user = useRpcQuery(rpc, 'get_user', id);
// SolidJS
const user = createRpcQuery(rpc, 'get_user', () => id);4 framework wrappers
Opt-in reactive wrappers for Svelte 5, React, Vue 3, and SolidJS. Auto-refetching queries, mutation lifecycle callbacks — generated alongside the base client.
Rich client
Retry with backoff, per-request timeout, AbortSignal support, request deduplication, lifecycle hooks, async headers — all configurable globally or per-call.
const rpc = createRpcClient({
baseUrl: '/api',
retry: 3,
timeout: 5000,
headers: () => ({ Authorization: getToken() }),
onError: (e) => logError(e),
});