<div align="center">
# β‘ vercel-rpc
**End-to-end typesafe RPC between Rust lambdas on Vercel and SvelteKit**
[**Live Demo β** svelte-rust-beta.vercel.app](https://svelte-rust-beta.vercel.app)
[](https://github.com/misha-mad/vercel-rpc/actions/workflows/ci.yml)
[](./crates)
[](./demo/tests/integration)
[](./demo/tests/e2e)
[](./demo/src/lib/rpc-types.ts)
[](https://vercel.com)
[](#license)
Write Rust functions β get a fully typed TypeScript client. Zero config, zero boilerplate.
</div>
---
## Why?
Building serverless APIs with Rust on Vercel is fast β but keeping TypeScript types in sync is painful. **vercel-rpc** solves this:
- π¦ **Write plain Rust functions** with `#[rpc_query]` / `#[rpc_mutation]`
- π **Auto-generate TypeScript types & client** from Rust source code
- π **Watch mode** β types regenerate on every save
- π **Deploy to Vercel** β each function becomes a serverless lambda
- π‘οΈ **End-to-end type safety** β Rust types β TypeScript types, no manual sync
## How It Works
```
ββββββββββββββββ scan βββββββββββββββ codegen ββββββββββββββββββββ
β api/*.rs β βββββββββββΆ β Manifest β βββββββββββΆ β rpc-types.ts β
β #[rpc_query]β (syn) β procedures β (rustβts) β rpc-client.ts β
β #[rpc_mut.] β β structs β β Typed RpcClient β
ββββββββββββββββ βββββββββββββββ ββββββββββββββββββββ
β β
β deploy (vercel) import (svelte) β
βΌ βΌ
ββββββββββββββββ HTTP (GET/POST) ββββββββββββββββββββ
β Vercel Lambdaβ ββββββββββββββββββββββββββββββββββββββββ β SvelteKit App β
β /api/hello β β rpc.query(...) β
β /api/time β ββββββββββββββββββββββββββββββββββββββββΆ β fully typed! β¨ β
ββββββββββββββββ JSON response ββββββββββββββββββββ
```
## Quick Start
### 1. Define a Rust lambda
```rust
// api/hello.rs
use vercel_rpc_macro::rpc_query;
#[rpc_query]
async fn hello(name: String) -> String {
format!("Hello, {} from Rust on Vercel!", name)
}
```
That's it. The macro generates a full Vercel-compatible handler with:
- Input parsing (query params for queries, JSON body for mutations)
- JSON serialization of the response
- CORS headers & OPTIONS preflight
- HTTP method validation (GET for queries, POST for mutations)
- Structured error responses for `Result<T, E>` return types
### 2. Generate TypeScript bindings
```bash
# One-time generation (from demo/)
cd demo
npm run generate
# Or directly with cargo (from project root)
cargo run -p vercel-rpc-cli -- generate --dir api --output demo/src/lib/rpc-types.ts --client-output demo/src/lib/rpc-client.ts
```
This produces two files:
**`src/lib/rpc-types.ts`** β type definitions:
```typescript
export interface TimeResponse {
timestamp: number;
message: string;
}
export type Procedures = {
queries: {
hello: { input: string; output: string };
time: { input: void; output: TimeResponse };
};
mutations: {
};
};
```
**`src/lib/rpc-client.ts`** β typed client with overloads:
```typescript
export interface RpcClient {
query(key: "time"): Promise<TimeResponse>;
query(key: "hello", input: string): Promise<string>;
}
export function createRpcClient(baseUrl: string): RpcClient { /* ... */ }
```
### 3. Use in SvelteKit
```typescript
// demo/src/lib/client.ts
import { createRpcClient } from "./rpc-client";
export const rpc = createRpcClient("/api");
```
```svelte
<script lang="ts">
import { rpc } from "$lib/client";
let greeting = $state("");
async function sayHello() {
greeting = await rpc.query("hello", "World");
}
</script>
<button onclick={sayHello}>Say Hello</button>
<p>{greeting}</p>
```
### 4. Watch mode (development)
```bash
cd demo
npm run dev
```
This runs the RPC watcher and Vite dev server in parallel. Every time you save a `.rs` file in `api/`, the TypeScript types and client are regenerated automatically:
```
vercel-rpc watch mode
api dir: api
types: src/lib/rpc-types.ts
client: src/lib/rpc-client.ts
β Generated 2 procedure(s), 1 struct(s), 0 enum(s) in 3ms
β src/lib/rpc-types.ts
β src/lib/rpc-client.ts
Watching for changes in api
[12:34:56] Changed: api/hello.rs
β Regenerated in 2ms
```
## Project Structure
```
vercel-rpc/
βββ crates/
β βββ rpc-macro/ # Proc-macro crate
β β βββ src/lib.rs # #[rpc_query] / #[rpc_mutation]
β βββ rpc-cli/ # CLI crate (binary: `rpc`)
β βββ src/
β βββ main.rs # CLI entry (scan / generate / watch)
β βββ model.rs # Manifest, Procedure, RustType, StructDef, EnumDef
β βββ parser/ # Rust source β Manifest (via syn)
β β βββ extract.rs # File scanning & procedure extraction
β β βββ types.rs # syn::Type β RustType conversion
β βββ codegen/ # Manifest β TypeScript
β β βββ typescript.rs # RustType β TS type mapping + rpc-types.ts
β β βββ client.rs # RpcClient interface + rpc-client.ts
β βββ watch.rs # File watcher with debounce
βββ demo/ # SvelteKit demo application + Rust lambdas
β βββ api/ # Rust lambdas (each file = one endpoint)
β β βββ hello.rs # GET /api/hello?input="name"
β β βββ time.rs # GET /api/time
β βββ Cargo.toml # Rust package for demo lambdas
β βββ src/
β β βββ lib/
β β β βββ rpc-types.ts # β auto-generated types
β β β βββ rpc-client.ts # β auto-generated client
β β β βββ client.ts # RPC client instance (manual)
β β βββ routes/ # SvelteKit pages
β βββ tests/
β β βββ integration/ # Vitest: codegen pipeline tests
β β βββ e2e/ # Playwright: UI + API tests
β βββ package.json # Node scripts
β βββ svelte.config.js # SvelteKit config
β βββ vite.config.ts # Vite config + API mock plugin
β βββ tsconfig.json # TypeScript config
βββ Cargo.toml # Rust workspace (crates + demo)
βββ vercel.json # Vercel config
βββ README.md
```
## CLI Reference
### `rpc scan`
Scan Rust source files and print discovered procedures:
```bash
cargo run -p vercel-rpc-cli -- scan --dir api
```
```
Discovered 2 procedure(s), 1 struct(s), 0 enum(s):
Query hello (String) -> String [api/hello.rs]
Query time (()) -> TimeResponse [api/time.rs]
struct TimeResponse {
timestamp: u64,
message: String,
}
```
### `rpc generate`
Generate TypeScript types and client:
```bash
cargo run -p vercel-rpc-cli -- generate \
--dir api \
--output src/lib/rpc-types.ts \
--client-output src/lib/rpc-client.ts \
--types-import ./rpc-types
```
| `--dir`, `-d` | `api` | Rust source directory |
| `--output`, `-o` | `src/lib/rpc-types.ts` | Types output path |
| `--client-output`, `-c` | `src/lib/rpc-client.ts` | Client output path |
| `--types-import` | `./rpc-types` | Import path for types in client |
### `rpc watch`
Watch for changes and regenerate on save (same flags as `generate`):
```bash
cargo run -p vercel-rpc-cli -- watch --dir api
```
## Rust Macros
### `#[rpc_query]` β GET endpoint
```rust
use vercel_rpc_macro::rpc_query;
// No input
#[rpc_query]
async fn version() -> String {
"1.0.0".to_string()
}
// With input (parsed from ?input= query param)
#[rpc_query]
async fn hello(name: String) -> String {
format!("Hello, {}!", name)
}
// With custom struct output
#[rpc_query]
async fn time() -> TimeResponse {
TimeResponse { timestamp: 123, message: "now".into() }
}
// With Result return type (Err β 400 JSON error)
#[rpc_query]
async fn risky(id: u32) -> Result<Item, String> {
if id == 0 { Err("invalid id".into()) } else { Ok(Item { id }) }
}
```
### `#[rpc_mutation]` β POST endpoint
```rust
use vercel_rpc_macro::rpc_mutation;
#[rpc_mutation]
async fn create_item(input: CreateInput) -> Item {
// input is parsed from the JSON request body
Item { id: 1, name: input.name }
}
```
### Enum & Struct support
Structs and enums with `#[derive(Serialize)]` are automatically picked up and converted to TypeScript:
```rust
#[derive(Serialize)]
struct UserProfile {
name: String,
age: u32,
}
#[derive(Serialize)]
enum Status {
Active,
Inactive,
Banned,
}
#[derive(Serialize)]
enum ApiResult {
Ok(String), // tuple variant
NotFound, // unit variant
Error { code: u32, message: String } // struct variant
}
```
Generated TypeScript:
```typescript
export interface UserProfile {
name: string;
age: number;
}
export type ApiResult = { Ok: string } | "NotFound" | { Error: { code: number; message: string } };
```
### Generated handler features
Every macro-annotated function automatically gets:
| **CORS** | `Access-Control-Allow-Origin: *` on all responses |
| **Preflight** | `OPTIONS` β `204 No Content` with CORS headers |
| **Method check** | `405 Method Not Allowed` for wrong HTTP method |
| **Input parsing** | Query param (GET) or JSON body (POST) |
| **Error handling** | `Result<T, E>` β `Ok` = 200, `Err` = 400 with JSON error |
| **Response format** | `{ "result": { "type": "response", "data": ... } }` |
## Type Mapping
| `String`, `&str`, `char` | `string` |
| `i8`..`i128`, `u8`..`u128`, `f32`, `f64` | `number` |
| `bool` | `boolean` |
| `()` | `void` |
| `Vec<T>` | `T[]` |
| `Option<T>` | `T \| null` |
| `HashMap<K, V>`, `BTreeMap<K, V>` | `Record<K, V>` |
| `(A, B, C)` | `[A, B, C]` |
| `Result<T, E>` | `T` (error handled at runtime) |
| Custom structs | `interface` with same fields |
| Enums (unit variants) | `"A" \| "B" \| "C"` (string union) |
| Enums (tuple variants) | `{ A: string } \| { B: number }` (tagged union) |
| Enums (struct variants) | `{ A: { x: number; y: number } }` (tagged union) |
| Enums (mixed) | Combination of all above |
## npm Scripts
See CONTRIBUTING.md for development scripts and setup instructions.
## Testing
Detailed testing strategy and commands are described in CONTRIBUTING.md.
## Deploy to Vercel
Since the SvelteKit demo lives in `demo/`, you need to configure Vercel's **Root Directory**:
1. Go to your Vercel project β **Settings** β **General**
2. Set **Root Directory** to `demo`
3. Vercel will auto-detect SvelteKit and run `npm run build` from `demo/`
4. Rust lambdas in `demo/api/` are compiled as serverless functions automatically
```bash
# Install Vercel CLI
npm i -g vercel
# Deploy (set root directory on first deploy)
vercel
```
> **Note:** With Root Directory set to `demo`, Vercel detects `demo/api/` as the serverless functions' directory. So `demo/api/hello.rs` β `/api/hello`.
Each `.rs` file in `api/` becomes a serverless function at `/api/<name>`.
## Sponsors
<div align="center">
<em>You could be the first sponsor! β€οΈ</em>
</div>
If you find this project useful, consider [sponsoring](https://github.com/sponsors/misha-mad) to support its development.
## License
MIT OR Apache-2.0
---
<sub>This project is not affiliated with or endorsed by Vercel Inc. "Vercel" is a trademark of Vercel Inc.</sub>