vercel-rpc-cli 0.1.1

CLI tool for Vercel RPC: parses Rust lambdas and generates TypeScript types and client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
<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)

[![CI](https://github.com/misha-mad/vercel-rpc/actions/workflows/ci.yml/badge.svg)](https://github.com/misha-mad/vercel-rpc/actions/workflows/ci.yml)
[![Rust Tests](https://img.shields.io/badge/rust_tests-60_passed-brightgreen?logo=rust)](./crates)
[![Vitest](https://img.shields.io/badge/vitest-12_passed-brightgreen?logo=vitest)](./demo/tests/integration)
[![Playwright](https://img.shields.io/badge/e2e-8_passed-brightgreen?logo=playwright)](./demo/tests/e2e)
[![TypeScript](https://img.shields.io/badge/types-auto--generated-blue?logo=typescript)](./demo/src/lib/rpc-types.ts)
[![Vercel](https://img.shields.io/badge/deploy-vercel-black?logo=vercel)](https://vercel.com)
[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](#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
<!-- 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)

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

| 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`):

```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 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**       | `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

| 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 β†’ **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>