REST APIs with RPC-like ergonomics.
Define REST APIs with methods, paths, request segments, response statuses, content types, and streaming—or start with sensible defaults. Call them through a typed client that feels like ordinary function calls.
rest-rpc puts HTTP first. Define routes alongside their handlers and infer
client types from their return values. Use your existing server framework,
middleware, and deployment model.
Use Only the Details You Need
// POST /todos/create, flat JSON input, plain result.
const const create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>
create = const route: ServerRouteBuilder<ExpressHandlerFields, DefaultContext>Entry point for declaring routes with handlers for Express.route
.input<z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>, undefined, "", never>(this: BuilderReceiver<"", never>, schema: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>, options?: undefined): RouteBuilderView<WithFlatInput<DerivedState, z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>, undefined>, ServerBuilderExtension<ExpressHandlerFields, DefaultContext>, "", never>
Declares one flat input value. GET inputs use query encoding; other methods use a body.input(import zz.function object<{
title: z.ZodString;
}>(shape?: {
title: z.ZodString;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>
object({ title: z.ZodStringtitle: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.handler<{
id: string;
title: string;
}>(handler: HandlerFor<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}>): HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & ... 4 more ... & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<...> & ... 1 more ... & {
...;
}>
handler(({ input: {
title: string;
}
input: { title: stringtitle } }) => function createTodo(title: string): {
id: string;
title: string;
}
createTodo(title: stringtitle));
// Explicit method, path, request segments, statuses, and response headers.
const const get: HandlerImplementation<{
readonly kind: "http";
readonly method: "GET";
} & {
readonly path: "/todos/:id";
} & {
request: Omit<Omit<Omit<EmptyObject, "params"> & Record<"params", z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query"> & Record<"query", z.ZodObject<{
uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>;
}, z.core.$strip>>, "headers"> & Record<"headers", z.ZodObject<{
"x-request-id": z.ZodOptional<z.ZodString>;
}, z.core.$strip>>;
} & {
...;
} & {
...;
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
...;
} | {
...;
}, Omit<...> & ... 1 more ... & {
...;
}>
get = const route: ServerRouteBuilder<ExpressHandlerFields, DefaultContext>Entry point for declaring routes with handlers for Express.route
.get<"/todos/:id">(path: "/todos/:id"): RouteBuilderView<HttpStateFor<"GET">, ServerBuilderExtension<ExpressHandlerFields, DefaultContext>, "/todos/:id", never>get("/todos/:id")
.params<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>, "/todos/:id", never>(this: BuilderReceiver<"/todos/:id", never>, schema: z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>): RouteBuilderView<WithRequest<HttpStateFor<"GET">, "params", z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>, "params">, ServerBuilderExtension<ExpressHandlerFields, DefaultContext>, "/todos/:id", never>
Declares path parameters.params(import zz.function object<{
id: z.ZodString;
}>(shape?: {
id: z.ZodString;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>
object({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.query<z.ZodObject<{
uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>;
}, z.core.$strip>, "/todos/:id", never>(this: BuilderReceiver<"/todos/:id", never>, schema: z.ZodObject<{
uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>;
}, z.core.$strip>): RouteBuilderView<WithRequest<WithRequest<HttpStateFor<"GET">, "params", z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>, "params">, "query", z.ZodObject<{
uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>;
}, z.core.$strip>, "query">, ServerBuilderExtension<...>, "/todos/:id", never>
Declares URL query parameters.query(import zz.function object<{
uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>;
}>(shape?: {
uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>;
}, z.core.$strip>
object({ uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>
uppercase: import zz.enum<readonly ["yes", "no"]>(values: readonly ["yes", "no"], params?: string | z.core.$ZodEnumParams): z.ZodEnum<{
yes: "yes";
no: "no";
}> (+1 overload)
export enum
enum(["yes", "no"]).ZodType<any, any, $ZodEnumInternals<{ yes: "yes"; no: "no"; }>>.optional(): z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>
optional() }))
.headers<z.ZodObject<{
"x-request-id": z.ZodOptional<z.ZodString>;
}, z.core.$strip>, "/todos/:id", never>(this: BuilderReceiver<"/todos/:id", never>, schema: z.ZodObject<{
"x-request-id": z.ZodOptional<z.ZodString>;
}, z.core.$strip>): RouteBuilderView<WithRequest<WithRequest<WithRequest<HttpStateFor<"GET">, "params", z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>, "params">, "query", z.ZodObject<{
uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>;
}, z.core.$strip>, "query">, "headers", z.ZodObject<...>, "headers">, ServerBuilderExtension<...>, "/todos/:id", never>
Declares request headers.headers(import zz.function object<{
"x-request-id": z.ZodOptional<z.ZodString>;
}>(shape?: {
"x-request-id": z.ZodOptional<z.ZodString>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
"x-request-id": z.ZodOptional<z.ZodString>;
}, z.core.$strip>
object({ "x-request-id": import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string().ZodType<any, any, $ZodStringInternals<string>>.optional(): z.ZodOptional<z.ZodString>optional() }))
.handler<{
readonly status: 404;
readonly body: {
readonly code: "TODO_NOT_FOUND";
};
readonly responseHeaders?: undefined;
} | {
readonly status: 200;
readonly responseHeaders: {
readonly "x-request-id": string | undefined;
};
readonly body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
}>(handler: HandlerFor<{
readonly kind: "http";
readonly method: "GET";
} & {
readonly path: "/todos/:id";
} & {
request: Omit<Omit<Omit<EmptyObject, "params"> & Record<"params", z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query"> & Record<...>, "headers"> & Record<...>;
} & {
...;
} & {
...;
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
readonly status: 404;
readonly body: {
readonly code: "TODO_NOT_FOUND";
};
readonly responseHeaders?: undefined;
} | {
readonly status: 200;
readonly responseHeaders: {
readonly "x-request-id": string | undefined;
};
readonly body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
}>): HandlerImplementation<...>
handler(({ params: {
id: string;
}
params, query: {
uppercase?: "yes" | "no" | undefined;
}
query, headers: {
"x-request-id"?: string | undefined;
}
headers }) => {
const const todo: {
id: string;
title: string;
} | undefined
todo = function findTodo(id: string): {
id: string;
title: string;
} | undefined
findTodo(params: {
id: string;
}
params.id: stringid);
if (!const todo: {
id: string;
title: string;
} | undefined
todo) {
return { status: 404status: 404, body: {
readonly code: "TODO_NOT_FOUND";
}
body: { code: "TODO_NOT_FOUND"code: "TODO_NOT_FOUND" as type const = "TODO_NOT_FOUND"const } };
}
return {
status: 200status: 200,
responseHeaders: {
readonly "x-request-id": string | undefined;
}
responseHeaders: { "x-request-id": headers: {
"x-request-id"?: string | undefined;
}
headers["x-request-id"] },
body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
}
body: {
...const todo: {
id: string;
title: string;
}
todo,
title: stringtitle:
query: {
uppercase?: "yes" | "no" | undefined;
}
query.uppercase?: "yes" | "no" | undefineduppercase === "yes" ? const todo: {
id: string;
title: string;
}
todo.title: stringtitle.String.toUpperCase(): stringConverts all the alphabetic characters in a string to uppercase.toUpperCase() : const todo: {
id: string;
title: string;
}
todo.title: stringtitle,
},
};
});
export const const routes: {
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}
routes = { todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
}
todos: { create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: z.ZodObject<{
title: z.ZodString;
}, z.core.$strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>
create, get: HandlerImplementation<{
readonly kind: "http";
readonly method: "GET";
} & {
readonly path: "/todos/:id";
} & {
request: Omit<Omit<Omit<EmptyObject, "params"> & Record<"params", z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>, "query"> & Record<"query", z.ZodObject<{
uppercase: z.ZodOptional<z.ZodEnum<{
yes: "yes";
no: "no";
}>>;
}, z.core.$strip>>, "headers"> & Record<"headers", z.ZodObject<{
"x-request-id": z.ZodOptional<z.ZodString>;
}, z.core.$strip>>;
} & {
...;
} & {
...;
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
...;
} | {
...;
}, Omit<...> & ... 1 more ... & {
...;
}>
get } };import { function initClient<TContract extends Contract, const TGlobalHeaders extends Record<string, string> = Record<never, string>>(contract: TContract, options: ApiClientOptions<TGlobalHeaders>): ApiClientFor<TContract, TGlobalHeaders>Creates a typed fetch client whose shape mirrors a contract.initClient } from "@rest-rpc/core";
import { function generateContractFromType<TContract extends Contract>(options: generateContractFromTypeOptions): TContractGenerates a minimal JSON-compatible contract from an exported server route tree type.generateContractFromType } from "@rest-rpc/core/generate";
import type { const routes: {
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}
routes } from "./server";
// Run generation at build time for browser clients.
const const api: {
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}
api = generateContractFromType<{
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}>(options: generateContractFromTypeOptions): {
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}
Generates a minimal JSON-compatible contract from an exported server route tree type.generateContractFromType<typeof const routes: {
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}
routes>({
filePath: stringPath to the TypeScript source file that exports the server route tree type.filePath: "./server.ts",
exportName: stringName of the exported server route tree to generate a contract from.exportName: "routes",
});
const const client: {
todos: {
create: FetchResponseFn<Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
} & {
...;
}, Record<...>>;
get: FetchResponseFn<...>;
};
}
client = initClient<{
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}, Record<...>>(contract: {
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}, options: ApiClientOptions<...>): {
...;
}
Creates a typed fetch client whose shape mirrors a contract.initClient(const api: {
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}
api, { baseUrl: stringbaseUrl: "http://localhost:3000" });
const const todo: {
id: string;
title: string;
}
todo = await const client: {
todos: {
create: FetchResponseFn<Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
} & {
...;
}, Record<...>>;
get: FetchResponseFn<...>;
};
}
client.todos: {
create: FetchResponseFn<Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
} & {
...;
}, Record<...>>;
get: FetchResponseFn<...>;
}
todos.create: (request: {
title: string;
}, options?: FetchOptionsFor<Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
responses: {
200: {
body: ClientSchema<{
id: string;
title: string;
}>;
contentType: "application/json";
};
};
} & {
output: "output";
} & {
readonly handler: HandlerFor<...>;
}> | undefined) => Promise<...>
create({ title: stringtitle: "Write docs" });
const const response: {
status: 404;
body: {
readonly code: "TODO_NOT_FOUND";
};
headers: Headers;
} | {
status: 200;
body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
responseHeaders: SerializedResponseHeaders<{
readonly "x-request-id": string | undefined;
}>;
headers: Headers;
}
response = await const client: {
todos: {
create: FetchResponseFn<Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
} & {
...;
}, Record<...>>;
get: FetchResponseFn<...>;
};
}
client.todos: {
create: FetchResponseFn<Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
} & {
...;
}, Record<...>>;
get: FetchResponseFn<...>;
}
todos.get: (request: {
query: {
uppercase?: "yes" | "no" | undefined;
};
params: {
id: string;
};
headers?: {
"x-request-id"?: string | undefined;
} | undefined;
}, options?: FetchOptionsFor<Omit<{
readonly kind: "http";
readonly method: "GET";
} & {
readonly path: "/todos/:id";
} & {
request: Omit<Omit<Omit<EmptyObject, "params"> & Record<"params", ZodObject<{
id: ZodString;
}, $strip>>, "query"> & Record<"query", ZodObject<{
uppercase: ZodOptional<ZodEnum<{
yes: "yes";
no: "no";
}>>;
}, $strip>>, "headers"> & Record<...>;
} & {
...;
} & {
...;
} & EmptyObject, "responses"> & {
...;
} & {
...;
} & {
...;
}> | undefined) => Promise<...>
get({
params: {
id: string;
}
params: { id: stringid: const todo: {
id: string;
title: string;
}
todo.id: stringid },
query: {
uppercase?: "yes" | "no" | undefined;
}
query: { uppercase?: "yes" | "no" | undefineduppercase: "yes" },
headers?: {
"x-request-id"?: string | undefined;
} | undefined
headers: { "x-request-id": "req_1" },
});
if (const response: {
status: 404;
body: {
readonly code: "TODO_NOT_FOUND";
};
headers: Headers;
} | {
status: 200;
body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
responseHeaders: SerializedResponseHeaders<{
readonly "x-request-id": string | undefined;
}>;
headers: Headers;
}
response.status: 404 | 200status === 200) {
var console: Consoleconsole.Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(const response: {
status: 200;
body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
responseHeaders: SerializedResponseHeaders<{
readonly "x-request-id": string | undefined;
}>;
headers: Headers;
}
response.body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
}
body.title: stringtitle);
}import { function createTanstackQueryHelpers<TContract extends Contract, const TGlobalHeaders extends Record<string, string> = Record<never, string>>(contract: TContract, options: CreateTanstackQueryHelpersOptions<TGlobalHeaders>): TanstackQueryHelpersFor<TContract>Creates a TanStack Query helper tree that mirrors a contract.createTanstackQueryHelpers } from "@rest-rpc/tanstack-query";
import { function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<NoInfer<TData>, TError> (+2 overloads)useQuery, function useMutation<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(options: UseMutationOptions<TData, TError, TVariables, TOnMutateResult>, queryClient?: QueryClient): UseMutationResult<TData, TError, TVariables, TOnMutateResult>useMutation } from "@tanstack/react-query";
const const tq: TanstackQueryTreeFor<{
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}>
tq = createTanstackQueryHelpers<{
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}, Record<...>>(contract: {
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}, options: CreateTanstackQueryHelpersOptions<...>): TanstackQueryTreeFor<...>
Creates a TanStack Query helper tree that mirrors a contract.createTanstackQueryHelpers(const api: {
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}
api, {
baseUrl: stringbaseUrl: "http://localhost:3000",
});
const const todo: UseQueryResult<NoInfer<{
status: 200;
body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
responseHeaders: SerializedResponseHeaders<{
readonly "x-request-id": string | undefined;
}>;
headers: Headers;
}>, DeclaredRouteQueryError<Omit<{
readonly kind: "http";
readonly method: "GET";
} & {
readonly path: "/todos/:id";
} & {
request: Omit<Omit<Omit<EmptyObject, "params"> & Record<"params", ZodObject<{
id: ZodString;
}, $strip>>, "query"> & Record<"query", ZodObject<...>>, "headers"> & Record<...>;
} & {
...;
} & {
...;
} & EmptyObject, "responses"> & {
...;
} & {
...;
} & {
...;
}>>
todo = useQuery<{
status: 200;
body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
responseHeaders: SerializedResponseHeaders<{
readonly "x-request-id": string | undefined;
}>;
headers: Headers;
}, DeclaredRouteQueryError<Omit<{
readonly kind: "http";
readonly method: "GET";
} & {
readonly path: "/todos/:id";
} & {
request: Omit<Omit<Omit<EmptyObject, "params"> & Record<"params", ZodObject<{
id: ZodString;
}, $strip>>, "query"> & Record<"query", ZodObject<{
uppercase: ZodOptional<ZodEnum<{
...;
}>>;
}, $strip>>, "headers"> & Record<...>;
} & {
...;
} & {
...;
} & EmptyObject, "responses"> & {
...;
} & {
...;
} & {
...;
}>, {
status: 200;
body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
responseHeaders: SerializedResponseHeaders<{
readonly "x-request-id": string | undefined;
}>;
headers: Headers;
}, readonly unknown[]>(options: UndefinedInitialDataOptions<...>, queryClient?: QueryClient): UseQueryResult<...> (+2 overloads)
useQuery(
const tq: TanstackQueryTreeFor<{
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}>
tq.todos: TanstackQueryTreeFor<{
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
}>
todos.get: TanstackQueryRouteValue<HandlerImplementation<{
readonly kind: "http";
readonly method: "GET";
} & {
readonly path: "/todos/:id";
} & {
request: Omit<Omit<Omit<EmptyObject, "params"> & Record<"params", ZodObject<{
id: ZodString;
}, $strip>>, "query"> & Record<"query", ZodObject<...>>, "headers"> & Record<...>;
} & {
...;
} & {
...;
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
...;
} | {
...;
}, Omit<...> & ... 1 more ... & {
...;
}>>
get.queryOptions: <{
status: 200;
body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
responseHeaders: SerializedResponseHeaders<{
readonly "x-request-id": string | undefined;
}>;
headers: Headers;
}>(request: {
query: {
uppercase?: "yes" | "no" | undefined;
};
params: {
id: string;
};
headers?: {
"x-request-id"?: string | undefined;
} | undefined;
} | unique symbol, options?: QueryOptionsFor<HandlerImplementation<{
readonly kind: "http";
readonly method: "GET";
} & {
readonly path: "/todos/:id";
} & {
request: Omit<Omit<Omit<EmptyObject, "params"> & Record<...>, "query"> & Record<...>, "headers"> & Record<...>;
} & {
...;
} & {
...;
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
...;
} | {
...;
}, Omit<...> & ... 1 more ... & {
...;
}>, {
status: 200;
body: {
readonly title: string;
readonly id: string;
readonly code?: undefined;
};
responseHeaders: SerializedResponseHeaders<{
readonly "x-request-id": string | undefined;
}>;
headers: Headers;
}> | undefined) => QueryOptionsResultFor<...>
queryOptions({
params: {
id: string;
}
params: { id: stringid: "todo_1" },
query: {
uppercase?: "yes" | "no" | undefined;
}
query: { uppercase?: "yes" | "no" | undefineduppercase: "yes" },
}),
);
const const create: UseMutationResult<{
id: string;
title: string;
}, Error | HttpError, {
title: string;
}, unknown>
create = useMutation<{
id: string;
title: string;
}, Error | HttpError, {
title: string;
}, unknown>(options: UseMutationOptions<{
id: string;
title: string;
}, Error | HttpError, {
title: string;
}, unknown>, queryClient?: QueryClient): UseMutationResult<{
id: string;
title: string;
}, Error | HttpError, {
title: string;
}, unknown>
useMutation(const tq: TanstackQueryTreeFor<{
todos: {
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
};
}>
tq.todos: TanstackQueryTreeFor<{
create: HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>;
get: HandlerImplementation<...>;
}>
todos.create: TanstackQueryRouteValue<HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>>
create.mutationOptions: (options?: MutationOptionsFor<HandlerImplementation<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, ExpressHandlerFields, DefaultContext, {
id: string;
title: string;
}, Omit<{
readonly kind: "procedure";
readonly method: "POST";
} & {
readonly path: "";
} & {
request: EmptyObject & {
body: ZodObject<{
title: ZodString;
}, $strip>;
contentType: "application/json";
};
} & {
responses: EmptyObject;
} & {
readonly input: "input";
} & EmptyObject, "responses"> & {
...;
} & {
...;
}>> | undefined) => MutationOptions<...>
mutationOptions());
const create: UseMutationResult<{
id: string;
title: string;
}, Error | HttpError, {
title: string;
}, unknown>
create.mutate: (variables: {
title: string;
}, options?: MutateOptions<{
id: string;
title: string;
}, Error | HttpError, {
title: string;
}, unknown> | undefined) => void
The mutation function you can call with variables to trigger the mutation and optionally hooks on additional callback options.mutate({ title: stringtitle: "Write docs" });curl http://localhost:3000/todos/create \
-H 'content-type: application/json' \
-d '{"title":"Write docs"}'
curl 'http://localhost:3000/todos/todo_1?uppercase=yes' \
-H 'x-request-id: req_1'Plain outputs reach the client as data. Returns with status reach it as HTTP
response envelopes. The builder keeps both shapes typed without making you
choose a separate route kind.
Start building
Define HTTP routes with handlers and call them from a typed client.
Open the quickstart
Learn the route builder
Choose inputs, outputs, HTTP details, streams, and metadata in one builder.
Explore route declarations
Generate OpenAPI
Generate an OpenAPI document from your route schemas and metadata.
View OpenAPI docs
npx skills add rest-rpc/rest-rpc
What You Get
REST APIs
Build methods, paths, request locations, response bodies, and metadata with a fluent API that keeps invalid combinations out of route declarations.
Optional shared contracts
Declare an HTTP interface separately when it should be shared independently of the server implementation.
Composable route builders
Choose a method and path, flat input or request segments, and plain output or status responses independently in one fluent builder.
RPC-like ergonomics
Call routes and implement handlers as ordinary functions without compromising HTTP semantics. Your server works exactly like it did before, just with full type safety at the API boundary.
Server adapters
Use Express, Hono, Fastify, NestJS, Node HTTP, or fetch-native runtimes with your existing middleware and application code.
Typed responses
Return direct success bodies for simple routes, or status-keyed response envelopes when the route has multiple expected outcomes.
Schema options
Use Standard Schema-compatible libraries such as Zod, Valibot, or ArkType, or built-in type-only helper when runtime validation is not needed.
Generated helpers
Derive typed fetch clients, TanStack Query options, query keys, and OpenAPI documents from the same route tree.
Beyond basic JSON
Use typed streams, custom bodies, response headers, and content types in the same route builder.
What’s Next
Build the first route
Start with the Quickstart if you want the fastest end-to-end example.
Shape the routes
Use the Route Builder to choose the HTTP details your API needs.
Choose the runtime
Explore the server adapters, the fetch client, or TanStack Query.
Describe the API
Generate an OpenAPI document without ceremony.
Common Questions
Who is rest-rpc for?
rest-rpc is for developers who want end-to-end type safety and RPC-like
ergonomics without replacing familiar HTTP concepts. You keep methods,
paths, headers, bodies, status codes, and your existing framework, while
route declarations connect server and client types.
Do I have to restructure my app?
No. Keep your framework, middleware, authentication, database access, and deployment model. Add typed routes at the API boundary and adopt the client, TanStack Query helpers, or OpenAPI generation as they become useful.
Do I need to learn a new API model?
The model is ordinary HTTP: methods, paths, request locations, status codes, headers, and response bodies. Choose explicit details when you need them or use defaults for simple routes.
Can I have a separate contract?
Yes. Declare the HTTP interface independently, implement it with any server adapter, and share it directly with clients. See the Contract-first Quickstart.
What happens when something breaks?
You debug the same boundary you would debug by hand: method, path, query, headers, body, status, and response shape. Because those pieces live in one route declaration, type errors and validation errors point back to that declaration.