metaxy

/mɛˈtæk.si/ · Greek: μεταξύ · the in-between

End-to-end typesafe RPC between
Rust lambdas & TS frontend

rust
#[derive(Serialize)]
pub struct User {
    pub id: u64,
    pub name: String,
    pub active: bool,
}
ts
// 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.

ts
const rpc = createRpcClient({ baseUrl: '/api' });

const greeting = await rpc.query('hello', 'World');
//    ^ string — fully typed, with autocomplete
sh
$ 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.

rust
#[rpc_query]
async fn hello(name: String) -> String {
    format!("Hello, {}!", name)
}
rust
#[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.

rust
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UserProfile {
    pub display_name: String,
    pub email: String,
    #[serde(skip)]
    pub password_hash: String,
}
rust
#[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.

sh
my-app/
├── api/
   ├── hello.rs /api/hello
   └── users.rs /api/users
├── src/             # frontend (any framework)
├── Cargo.toml
└── vercel.json
ts
// 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.

ts
const rpc = createRpcClient({
  baseUrl: '/api',
  retry: 3,
  timeout: 5000,
  headers: () => ({ Authorization: getToken() }),
  onError: (e) => logError(e),
});

visit GitHub to learn more about metaxy