vercel-rpc-cli 0.1.0

CLI tool for Vercel RPC: parses Rust lambdas and generates TypeScript types and client
vercel-rpc-cli-0.1.0 is not a library.
Visit the last successful build: vercel-rpc-cli-0.4.0

⚡ vercel-rpc

End-to-end typesafe RPC between Rust lambdas on Vercel and SvelteKit

Live Demo → svelte-rust-beta.vercel.app

CI Rust Tests Vitest Playwright TypeScript Vercel License: MIT

Write Rust functions → get a fully typed TypeScript client. Zero config, zero boilerplate.


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

// 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

# 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:

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:

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

// demo/src/lib/client.ts
import { createRpcClient } from "./rpc-client";
export const rpc = createRpcClient("/api");
<!-- demo/src/routes/+page.svelte -->
<script lang="ts">
  import { rpc } from "$lib/client";

  let greeting = $state("");

  async function sayHello() {
    greeting = await rpc.query("hello", "World");
    //                  ^ autocomplete ✨
    //                         ^ typed as string ✨
  }
</script>

<button onclick={sayHello}>Say Hello</button>
<p>{greeting}</p>

4. Watch mode (development)

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:

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:

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
Flag Default Description
--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):

cargo run -p vercel-rpc-cli -- watch --dir api

Rust Macros

#[rpc_query] — GET endpoint

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

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:

#[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:

export interface UserProfile {
  name: string;
  age: number;
}

export type Status = "Active" | "Inactive" | "Banned";

export type ApiResult = { Ok: string } | "NotFound" | { Error: { code: number; message: string } };

Generated handler features

Every macro-annotated function automatically gets:

Feature Description
CORS Access-Control-Allow-Origin: * on all responses
Preflight OPTIONS204 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

Rust TypeScript
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 → SettingsGeneral
  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
# 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

If you find this project useful, consider sponsoring to support its development.

License

MIT OR Apache-2.0


This project is not affiliated with or endorsed by Vercel Inc. "Vercel" is a trademark of Vercel Inc.