vercel-rpc-cli
CLI that scans Rust lambda source files annotated with #[rpc_query] /
#[rpc_mutation] and generates TypeScript type definitions and a fully typed
RPC client.
Part of the vercel-rpc project.
Installation
This installs the rpc binary.
Commands
rpc scan
Parse Rust source files and print discovered procedures, structs, and enums:
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,
}
Also outputs a JSON manifest for tooling consumption.
rpc generate
Generate TypeScript types and a typed client from Rust source files:
This produces two files:
rpc-types.ts — TypeScript interfaces and a Procedures type map:
export interface TimeResponse {
timestamp: number;
message: string;
}
export type Procedures = {
queries: {
hello: { input: string; output: string };
time: { input: void; output: TimeResponse };
};
mutations: {};
};
rpc-client.ts — a typed RpcClient with method overloads:
export interface RpcClient {
query(key: "time"): Promise<TimeResponse>;
query(key: "hello", input: string): Promise<string>;
}
export function createRpcClient(baseUrl: string): RpcClient;
rpc watch
Watch for .rs file changes and regenerate automatically (same flags as
generate):
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) in 3ms
→ src/lib/rpc-types.ts
→ src/lib/rpc-client.ts
Watching for changes in api
Changes are debounced (200 ms by default, configurable via rpc.config.toml).
Press Ctrl+C to stop.
Use --clear-screen to clear the terminal before each regeneration cycle:
Configuration
The CLI can be configured with an optional rpc.config.toml file. Place it at your project root (next to Cargo.toml or package.json). All fields are optional — defaults match the CLI flags below.
# rpc.config.toml
[]
= "api" # Rust source directory to scan
= ["**/*.rs"] # glob patterns for files to include
= [] # glob patterns for files to exclude
[]
= "src/lib/rpc-types.ts" # generated types file path
= "src/lib/rpc-client.ts" # generated client file path
[]
= "./rpc-types" # import specifier used in client file
= "" # suffix appended to import (e.g. ".js" for ESM)
[]
= false # forward Rust `///` doc comments as JSDoc
[]
= "preserve" # "preserve" (default) or "camelCase"
[]
= 200 # file watcher debounce interval (ms)
= false # clear terminal before each regeneration
include and exclude accept glob patterns matched against file paths relative to dir. A file must match at least one include pattern and no exclude pattern to be scanned. When both match, exclude wins.
Preserving doc comments
When preserve_docs = true in [codegen], Rust /// doc comments are forwarded as JSDoc (/** ... */) in the generated TypeScript files. This is useful for editor tooltips and documentation.
Given this Rust source:
/// Returns the current server time.
async
/// A timestamp with a human-readable message.
/// Possible request statuses.
With preserve_docs = true, the generated rpc-types.ts includes:
/** A timestamp with a human-readable message. */
export interface TimeResponse {
timestamp: number;
message: string;
}
/** Possible request statuses. */
export type Status = "Active" | "Inactive";
export type Procedures = {
queries: {
/** Returns the current server time. */
time: { input: void; output: TimeResponse };
};
mutations: {};
};
And the generated rpc-client.ts includes JSDoc on overloads:
export interface RpcClient {
/** Returns the current server time. */
query(key: "time"): Promise<TimeResponse>;
}
Multi-line doc comments are preserved as multi-line JSDoc:
/// Greet a user by name.
/// Returns a personalized greeting string.
async
/**
* Greet a user by name.
* Returns a personalized greeting string.
*/
export interface RpcClient {
// ...
}
With preserve_docs = false (the default), doc comments are silently ignored and no JSDoc is emitted.
Field naming
The [codegen.naming] section controls how struct field names appear in the generated TypeScript.
| Value | Behavior | Example |
|---|---|---|
"preserve" (default) |
Keep Rust field names as-is | uptime_secs → uptime_secs |
"camelCase" |
Convert snake_case to camelCase | uptime_secs → uptimeSecs |
[]
= "camelCase"
Given this Rust source:
With fields = "preserve" (default):
export interface ServiceStatus {
uptime_secs: number;
api_version: string;
}
export type Event = { Click: { page_x: number; page_y: number } };
With fields = "camelCase":
export interface ServiceStatus {
uptimeSecs: number;
apiVersion: string;
}
export type Event = { Click: { pageX: number; pageY: number } };
The transform applies to struct interface fields and struct variant fields in enums. Enum variant names and procedure names are not affected.
Config discovery
The CLI walks up from the current directory looking for rpc.config.toml. If no file is found, built-in defaults are used.
# Use a specific config file
# Disable config file loading entirely
Resolution order
Values are resolved with this priority (highest first):
CLI flag > rpc.config.toml > built-in default
A config file sets project-level defaults; CLI flags override them per invocation.
Flags
| Flag | Short | Default | Commands | Description |
|---|---|---|---|---|
--dir |
-d |
api |
scan, generate, watch | Rust source directory to scan |
--include |
**/*.rs |
scan, generate, watch | Glob pattern for files to include (repeatable) | |
--exclude |
(none) | scan, generate, watch | Glob pattern for files to exclude (repeatable) | |
--output |
-o |
src/lib/rpc-types.ts |
generate, watch | Output path for TypeScript types |
--client-output |
-c |
src/lib/rpc-client.ts |
generate, watch | Output path for TypeScript client |
--types-import |
./rpc-types |
generate, watch | Import path for types in the client file | |
--extension |
"" |
generate, watch | Suffix appended to types import (e.g. .js for ESM) |
|
--preserve-docs |
false |
generate, watch | Forward Rust doc comments as JSDoc | |
--fields |
preserve |
generate, watch | Field naming: preserve or camelCase |
|
--debounce-ms |
200 |
watch | File watcher debounce interval in milliseconds | |
--clear-screen |
false |
watch | Clear terminal before each regeneration | |
--config |
(auto-discover) | (global) | Path to config file | |
--no-config |
false |
(global) | Disable config file loading |
What gets scanned
The parser recognizes:
- Functions annotated with
#[rpc_query]or#[rpc_mutation]— extracted as RPC procedures with their input/output types. - Structs with
#[derive(Serialize)]— converted to TypeScript interfaces. - Enums with
#[derive(Serialize)]— converted to TypeScript union types (unit variants become string literals, tuple/struct variants become tagged objects).
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" |
| Enums (tuple variants) | { A: string } | { B: number } |
| Enums (struct variants) | { A: { x: number } } |
Generated client features
The generated rpc-client.ts includes:
RpcClientinterface with typed overloads for every procedure — full autocomplete and type checking.createRpcClient(baseUrl)factory function.RpcErrorclass withstatusanddatafields for structured error handling.rpcFetchhelper — usesGETwith?input=<JSON>for queries andPOSTwith JSON body for mutations. Unwraps theresult.dataenvelope automatically.
Related crates
vercel-rpc-macro— procedural macros (#[rpc_query],#[rpc_mutation]) that generate Vercel lambda handlers from plain async functions.
License
MIT OR Apache-2.0