Skip to content
rest-rpc
Esc
navigateopen⌘Jpreview
On this page

Concepts

The mental model behind rest-rpc.

rest-rpc keeps the API HTTP-shaped at the boundary and function-shaped in TypeScript usage.

The contract is the source of truth. It declares methods, paths, request locations, response statuses, metadata, and OpenAPI hints. Server adapters and clients are derived from that contract.

REST At The Boundary

Routes are still normal HTTP routes.

get: {
	method: "GET",
	path: "/todos/:id",
	request: {
		params: {
			id: z.string(),
		},
	},
	responses: {
		200: todoSchema,
		404: notFoundSchema,
	},
}

The method and path define the real API. The generated client is a typed way to call that API.

RPC At The Call Site

The contract already knows where each value belongs in the HTTP request.

Because of that, handlers and clients can use one flattened input object.

await api.todos.get.fetch({ id: "todo_1" });
get({ id }) {
	return todos.get(id);
}

The HTTP details are not removed. They are moved to the place where they are defined once: the contract.

Route Keys

The route tree gives each route a stable code path.

api.todos.get
api.todos.create

Those paths are used for generated handlers, clients, TanStack Query options, query keys, and type helpers.

HTTP routing is still driven by method and path.

Flattened Request Input

HTTP separates request data into locations:

  • params
  • query
  • headers
  • body

rest-rpc keeps those locations explicit in the contract, then flattens them for handler and client usage.

request: {
	params: {
		id: z.string(),
	},
	query: {
		includeCompleted: z.coerce.boolean().optional(),
	},
	headers: {
		"x-request-id": z.string().optional(),
	},
	body: {
		title: z.string(),
	},
}

The handler and client input is:

{
	id: string;
	includeCompleted?: boolean;
	"x-request-id"?: string;
	title: string;
}

rest-rpc has strict rules for duplicate keys. If a key is declared in multiple locations, the contract is invalid and the library will throw an error. Each key must be unique across all request locations to ensure clarity and prevent ambiguity in request handling.

Status-Keyed Responses

Responses are declared by HTTP status code.

responses: {
	200: todoSchema,
	404: z.object({
		code: z.literal("TODO_NOT_FOUND"),
	}),
}

There is no separate errors field. Declared non-2xx responses are normal declared API responses.

Use fetch() when a route has one successful response and the status is not part of normal control flow.

Use fetchResponse() when the status matters.

Shortcuts Are Limited

Shortcuts exist only when the contract makes the missing HTTP detail unambiguous.

If a route has exactly one successful response, a handler can return the success body directly.

return todo;

The handler can always return an explicit response envelope.

return {
	status: 404,
	body: { code: "TODO_NOT_FOUND" },
};

What rest-rpc Owns

rest-rpc owns the typed API boundary:

  • contract declarations
  • route validation
  • request validation
  • handler and client types
  • request serialization
  • response normalization
  • generated client surfaces
  • OpenAPI document generation
  • providing straightforward way for you to integrate with your framework and product concerns

What Your App Owns

Your application still owns normal framework and product concerns:

  • middleware
  • authentication
  • authorization
  • dependency injection
  • logging
  • database access
  • file storage
  • OpenAPI serving and UI
  • deployment
  • frontend state outside API calls

Was this page helpful?