Getting Started
Build your first end-to-end typesafe API with oRPC, from defining procedures to serving and calling them from a client.
Building an API usually means defining HTTP endpoints on the server, calling them from the client, and keeping both sides’ types in sync by hand. oRPC removes that gap: you write plain TypeScript functions on the server, and clients call them like local functions. Input is validated at runtime, types flow end to end, and there is no code generation step.
This guide takes the shortest path through oRPC:
- Define procedures (the functions of your API) and group them into a router.
- Serve the router over HTTP.
- Call it from a fully typed client.
Installation
Install the server and client packages, plus a schema library for validating input at runtime. This guide uses Zod, but Valibot, ArkType, and any other Standard Schema library work the same way.
npm install @orpc/server@beta @orpc/client@beta zodpnpm add @orpc/server@beta @orpc/client@beta zodyarn add @orpc/server@beta @orpc/client@beta zodbun add @orpc/server@beta @orpc/client@beta zodDefine a Router
A procedure is a function that clients can call remotely. Build one with the os builder (short for oRPC server): optionally describe the input it accepts with a schema, then implement it with .handler. A router is a plain object that groups procedures and gives each one its calling path, like planet.list.
import { const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os } from '@orpc/server'
import * as import zz from 'zod'
export const const listPlanets: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>
listPlanets = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os
.Builder<DefaultInitialContext & object, Record<never, never>>.handler<{
id: number;
name: string;
}[]>(handler: ProcedureHandler<DefaultInitialContext & object, unknown, {
id: number;
name: string;
}[], ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>
Defines the function that implements the procedure and completes the
chain, returning a callable procedure.handler(async () => {
// replace with your database query
return [
{ id: numberid: 1, name: stringname: 'Earth' },
{ id: numberid: 2, name: stringname: 'Mars' },
]
})
export const const findPlanet: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>
findPlanet = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os
.Builder<DefaultInitialContext & object, Record<never, never>>.input<z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>>(schema: z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>): BuilderWithInput<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, Record<never, never>>
Defines the input schema used to validate and type the procedure input.input(import zz.function object<{
id: z.ZodNumber;
}>(shape?: {
id: z.ZodNumber;
} | 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.ZodNumber;
}, z.core.$strip>
object({ id: z.ZodNumberid: import zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber() }))
.BuilderWithInput<DefaultInitialContext & object, object, ZodObject<{ id: ZodNumber; }, $strip>, Record<never, never>>['handler']<{
id: number;
name: string;
}>(handler: ProcedureHandler<DefaultInitialContext & object, {
id: number;
}, {
id: number;
name: string;
}, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>
Defines the function that implements the procedure and completes the
chain, returning a callable procedure.handler(async ({ input: {
id: number;
}
input }) => {
// replace with your database query
return { id: numberid: input: {
id: number;
}
input.id: numberid, name: stringname: 'Earth' }
})
export const const createPlanet: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<never, never>, never>
createPlanet = const os: Builder<DefaultInitialContext & object, Record<never, never>>The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler`
to define procedures, then compose them into routers.os
.Builder<DefaultInitialContext & object, Record<never, never>>.input<z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>(schema: z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>): BuilderWithInput<DefaultInitialContext & object, object, z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, Record<never, never>>
Defines the input schema used to validate and type the procedure input.input(import zz.function object<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}>(shape?: {
name: z.ZodString;
description: 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<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>
object({ name: z.ZodStringname: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(), description: z.ZodOptional<z.ZodString>description: 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() }))
.BuilderWithInput<DefaultInitialContext & object, object, ZodObject<{ name: ZodString; description: ZodOptional<ZodString>; }, $strip>, Record<...>>['handler']<{
name: string;
description?: string | undefined;
id: number;
}>(handler: ProcedureHandler<DefaultInitialContext & object, {
name: string;
description?: string | undefined;
}, {
name: string;
description?: string | undefined;
id: number;
}, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, Schema<...>, Record<...>, never>
Defines the function that implements the procedure and completes the
chain, returning a callable procedure.handler(async ({ input: {
name: string;
description?: string | undefined;
}
input }) => {
// replace with your database insert
return { id: numberid: 3, ...input: {
name: string;
description?: string | undefined;
}
input }
})
export const const router: {
planet: {
list: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: DecoratedProcedure<DefaultInitialContext & object, ... 4 more ..., never>;
};
}
router = {
planet: {
list: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: DecoratedProcedure<DefaultInitialContext & object, ... 4 more ..., never>;
}
planet: {
list: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>
list: const listPlanets: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>
listPlanets,
find: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>
find: const findPlanet: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>
findPlanet,
create: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<never, never>, never>
create: const createPlanet: DecoratedProcedure<DefaultInitialContext & object, object, z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<never, never>, never>
createPlanet,
},
}
A few things to notice:
.inputvalidates each call before your handler runs and typesinputinside it.listPlanetsskips it: a procedure without.inputsimply takes no arguments.- No
.outputschema is needed: the client’s result type flows straight from the handler’s return type. - Procedures can do much more: share middleware, require context such as an authenticated user, and declare typed errors. Learn more in the Procedure documentation.
Create a Server
Clients reach your router through an HTTP server. RPCHandler does the translation: it matches each incoming request to a procedure, validates the input, runs your handler, and sends the result back. This example uses Node’s built-in HTTP module. The same router also runs on Bun, Deno, and Cloudflare Workers through the Fetch API adapter.
import { function createServer<Request extends typeof IncomingMessage = typeof IncomingMessage, Response extends typeof ServerResponse = typeof ServerResponse>(requestListener?: RequestListener<Request, Response>): Server<Request, Response> (+1 overload)Returns a new instance of
{@link
Server
}
.
The `requestListener` is a function which is automatically
added to the `'request'` event.
```js
import http from 'node:http';
// Create a local server to receive data from
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});
server.listen(8000);
```
```js
import http from 'node:http';
// Create a local server to receive data from
const server = http.createServer();
// Listen to the request event
server.on('request', (request, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});
server.listen(8000);
```createServer } from 'node:http'
import { class RPCHandler<T extends Context>Serves an oRPC router over the RPC protocol using Node.js built-in
HTTP request/response objects.RPCHandler } from '@orpc/server/node'
const const handler: RPCHandler<DefaultInitialContext & object>handler = new new RPCHandler<DefaultInitialContext & object>(router: Router<DefaultInitialContext & object>, options?: NoInfer<RPCHandlerOptions<DefaultInitialContext & object>>): RPCHandler<DefaultInitialContext & object>Serves an oRPC router over the RPC protocol using Node.js built-in
HTTP request/response objects.RPCHandler(const router: {
planet: {
list: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: DecoratedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: DecoratedProcedure<DefaultInitialContext & object, ... 4 more ..., never>;
};
}
router)
const const server: Server<typeof IncomingMessage, typeof ServerResponse>server = createServer<typeof IncomingMessage, typeof ServerResponse>(requestListener?: RequestListener<typeof IncomingMessage, typeof ServerResponse> | undefined): Server<typeof IncomingMessage, typeof ServerResponse> (+1 overload)Returns a new instance of
{@link
Server
}
.
The `requestListener` is a function which is automatically
added to the `'request'` event.
```js
import http from 'node:http';
// Create a local server to receive data from
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});
server.listen(8000);
```
```js
import http from 'node:http';
// Create a local server to receive data from
const server = http.createServer();
// Listen to the request event
server.on('request', (request, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});
server.listen(8000);
```createServer(async (req: IncomingMessagereq, res: ServerResponse<IncomingMessage> & {
req: IncomingMessage;
}
res) => {
const { const matched: booleanmatched } = await const handler: RPCHandler<DefaultInitialContext & object>handler.NodeHttpHandler<DefaultInitialContext & object>.handle(request: NodeHttpRequest, response: NodeHttpResponse, options?: FriendlyStandardHandlerHandleOptions<DefaultInitialContext & object> | undefined): Promise<NodeHttpHandlerHandleResult>handle(req: IncomingMessagereq, res: ServerResponse<IncomingMessage> & {
req: IncomingMessage;
}
res, { prefix?: `/${string}` | undefinedprefix: '/rpc' })
if (const matched: booleanmatched) {
return
}
res: ServerResponse<IncomingMessage> & {
req: IncomingMessage;
}
res.ServerResponse<Request extends IncomingMessage = IncomingMessage>.statusCode: numberWhen using implicit headers (not calling `response.writeHead()` explicitly),
this property controls the status code that will be sent to the client when
the headers get flushed.
```js
response.statusCode = 404;
```
After response header was sent to the client, this property indicates the
status code which was sent out.statusCode = 404
res: ServerResponse<IncomingMessage> & {
req: IncomingMessage;
}
res.Stream.Writable.end(chunk: any, cb?: () => void): ServerResponse<IncomingMessage> & {
req: IncomingMessage;
} (+2 overloads)
Signals that no more data will be written, with one final chunk of data.end('Not found')
})
const server: Server<typeof IncomingMessage, typeof ServerResponse>server.Server.listen(port?: number, hostname?: string, listeningListener?: (() => void) | undefined): Server<typeof IncomingMessage, typeof ServerResponse> (+8 overloads)Start a server listening for connections. A `net.Server` can be a TCP or
an `IPC` server depending on what it listens to.
Possible signatures:
* `server.listen(handle[, backlog][, callback])`
* `server.listen(options[, callback])`
* `server.listen(path[, backlog][, callback])` for `IPC` servers
* `server.listen([port[, host[, backlog]]][, callback])` for TCP servers
This function is asynchronous. When the server starts listening, the `'listening'` event will be emitted. The last parameter `callback`will be added as a listener for the `'listening'`
event.
All `listen()` methods can take a `backlog` parameter to specify the maximum
length of the queue of pending connections. The actual length will be determined
by the OS through sysctl settings such as `tcp_max_syn_backlog` and `somaxconn` on Linux. The default value of this parameter is 511 (not 512).
All
{@link
Socket
}
are set to `SO_REUSEADDR` (see [`socket(7)`](https://man7.org/linux/man-pages/man7/socket.7.html) for
details).
The `server.listen()` method can be called again if and only if there was an
error during the first `server.listen()` call or `server.close()` has been
called. Otherwise, an `ERR_SERVER_ALREADY_LISTEN` error will be thrown.
One of the most common errors raised when listening is `EADDRINUSE`.
This happens when another server is already listening on the requested`port`/`path`/`handle`. One way to handle this would be to retry
after a certain amount of time:
```js
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
console.error('Address in use, retrying...');
setTimeout(() => {
server.close();
server.listen(PORT, HOST);
}, 1000);
}
});
```listen(3000, '127.0.0.1', () => 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('Listening on 127.0.0.1:3000'))
Every procedure is now reachable under the /rpc prefix. Requests that are not oRPC calls fall through, so you can handle them yourself, here with a plain 404. For CORS, logging, and other options, see the RPC Handler documentation.
Create a Client
On the client, RPCLink is the counterpart of RPCHandler: it turns your function calls into HTTP requests. Pass it to createORPCClient, and type the result with RouterClient<typeof router> so the client knows every procedure, its input, and its output.
import type { type RouterClient<TRouter extends AnyRouter, TClientContext extends ClientContext = object> = TRouter extends Procedure<any, any, infer $InputSchema extends AnySchema, infer $OutputSchema extends AnySchema, infer $ErrorMap extends ErrorMap, infer $ReturnedError extends AnyORPCError> ? ProcedureClient<TClientContext, $InputSchema, $OutputSchema, $ErrorMap, $ReturnedError> : { [K in keyof TRouter]: TRouter[K] extends Lazyable<infer U extends AnyRouter> ? RouterClient<...> : never; }The client type derived from a router, exposing every procedure as a callable
function while preserving the router's shape.RouterClient } from '@orpc/server'
import { function createORPCClient<T extends AnyNestedClient>(link: ClientLink<InferClientContext<T>>, { path, ...options }?: NoInfer<ORPCClientOptions<T>>): TCreates a fully typed oRPC client from a link.
The returned client mirrors the shape of your router or contract,
so calling a procedure is as simple as calling a function.createORPCClient } from '@orpc/client'
import { class RPCLink<T extends ClientContext>Client link that communicates with an RPC Handler over the Fetch API (HTTP).RPCLink } from '@orpc/client/fetch'
const const link: RPCLink<ClientContext>link = new new RPCLink<ClientContext>(options: RPCLinkOptions<ClientContext>): RPCLink<ClientContext>Client link that communicates with an RPC Handler over the Fetch API (HTTP).RPCLink({
FetchLinkTransportOptions<ClientContext>.origin?: Value<Promisable<`https://${string}` | `http://${string}` | ({} & string) | undefined>, [options: ClientOptions<ClientContext>, path: string[]]>The origin to prepend to all request URLs, useful for CORS requests.origin: 'http://127.0.0.1:3000',
RPCLinkCodecOptions<ClientContext>.url?: Value<Promisable<StandardUrl>, [options: ClientOptions<ClientContext>, path: string[], input: unknown]> | undefinedBase url for all requests (without origin). Should match with handler's prefix.url: '/rpc', // <- must match the server's prefix
})
export const const orpc: {
planet: {
list: ProcedureClient<object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: ProcedureClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<...>, never>;
};
}
orpc: type RouterClient<TRouter extends AnyRouter, TClientContext extends ClientContext = object> = TRouter extends Procedure<any, any, infer $InputSchema extends AnySchema, infer $OutputSchema extends AnySchema, infer $ErrorMap extends ErrorMap, infer $ReturnedError extends AnyORPCError> ? ProcedureClient<TClientContext, $InputSchema, $OutputSchema, $ErrorMap, $ReturnedError> : { [K in keyof TRouter]: TRouter[K] extends Lazyable<infer U extends AnyRouter> ? RouterClient<...> : never; }The client type derived from a router, exposing every procedure as a callable
function while preserving the router's shape.RouterClient<typeof const router: {
planet: {
list: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: DecoratedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: DecoratedProcedure<DefaultInitialContext & object, ... 4 more ..., never>;
};
}
router> = createORPCClient<{
planet: {
list: ProcedureClient<object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: ProcedureClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<...>, Record<...>, never>;
};
}>(link: ClientLink<...>, { path, ...options }?: NoInfer<ORPCClientOptions<...>>): {
planet: {
list: ProcedureClient<object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: ProcedureClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<...>, Record<...>, never>;
};
}
Creates a fully typed oRPC client from a link.
The returned client mirrors the shape of your router or contract,
so calling a procedure is as simple as calling a function.createORPCClient(const link: RPCLink<ClientContext>link)
When the caller runs in the same process as the server, for example during server-side rendering, skip HTTP entirely with a server-side client.
Call a Procedure
That is the whole setup. Call your procedures like local functions and let your editor do the rest:
const const planets: {
id: number;
name: string;
}[]
planets = await const orpc: {
planet: {
list: ProcedureClient<object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: ProcedureClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<...>, never>;
};
}
orpc.planet: {
list: ProcedureClient<object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: ProcedureClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<...>, never>;
}
planet.list: Client
(input?: void | undefined, options?: FriendlyClientOptions<object> | undefined) => PromiseWithError<{
id: number;
name: string;
}[], Error>
list()
const const planet: {
id: number;
name: string;
}
planet = await const orpc: {
planet: {
list: ProcedureClient<object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: ProcedureClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<...>, never>;
};
}
orpc.planet: {
list: ProcedureClient<object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: ProcedureClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<...>, never>;
}
planet.find: Client
(input: {
id: number;
}, options?: FriendlyClientOptions<object> | undefined) => PromiseWithError<{
id: number;
name: string;
}, Error>
find({ id: numberid: 1 })
const orpc: {
planet: {
list: ProcedureClient<object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: ProcedureClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<...>, never>;
};
}
orpc.planet: {
list: ProcedureClient<object, InitialInputSchema, Schema<{
id: number;
name: string;
}[]>, Record<never, never>, never>;
find: ProcedureClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, Schema<{
id: number;
name: string;
}>, Record<never, never>, never>;
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<...>, never>;
}
planet.- create
- find
- list
create: ProcedureClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, Schema<{
name: string;
description?: string | undefined;
id: number;
}>, Record<never, never>, never>
create
//
//
planet is typed from the handler’s return value, invalid input is rejected before your handler runs, and renaming a procedure on the server is a compile error in the client. There is no generated code to keep in sync.
Next Steps
- Learn the building blocks in depth: Procedure and Router
- Add authentication and logging with Middleware and Context, and reject calls with typed errors
- Stream typed events over Server-Sent Events (SSE) with AsyncIteratorObject
- Expose the same router as a REST API with the OpenAPI Handler and generate its OpenAPI specification
- Define your API as a contract first, then let TypeScript enforce the implementation
- Integrate with your stack: TanStack Query, SWR, Pinia Colada, Next.js, and NestJS