Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

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:

  1. Define procedures (the functions of your API) and group them into a router.
  2. Serve the router over HTTP.
  3. 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 zod
pnpm add @orpc/server@beta @orpc/client@beta zod
yarn add @orpc/server@beta @orpc/client@beta zod
bun add @orpc/server@beta @orpc/client@beta zod

Define 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.
@see{@link https://orpc.dev/docs/procedure Procedure}
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.
@see{@link https://orpc.dev/docs/procedure Procedure}
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.
@see{@link https://orpc.dev/docs/procedure 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.
@see{@link https://orpc.dev/docs/procedure Procedure}
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.
@see{@link https://orpc.dev/docs/procedure#inputoutput-validation Procedure - Input/Output Validation}
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.
@see{@link https://orpc.dev/docs/procedure 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.
@see{@link https://orpc.dev/docs/procedure Procedure}
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.
@see{@link https://orpc.dev/docs/procedure#inputoutput-validation Procedure - Input/Output Validation}
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.
@see{@link https://orpc.dev/docs/procedure 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:

  • .input validates each call before your handler runs and types input inside it. listPlanets skips it: a procedure without .input simply takes no arguments.
  • No .output schema 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); ```
@sincev0.1.13
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.
@see{@link https://orpc.dev/docs/adapters/node-http Node HTTP Adapter}
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.
@see{@link https://orpc.dev/docs/adapters/node-http Node HTTP Adapter}
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); ```
@sincev0.1.13
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: number
When 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.
@sincev0.4.0
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.
@see{@link Writable.end} for full details.@sincev0.9.4@paramchunk Optional data to write. For streams not operating in object mode, `chunk` must be a {string}, {Buffer}, {TypedArray} or {DataView}. For object mode streams, `chunk` may be any JavaScript value other than `null`.@paramcb Callback for when the stream is finished.
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[]): void
The **`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.
@see{@link https://orpc.dev/docs/client/client-side#creating-a-client Client-Side Clients - Creating a Client}
RouterClient
} from '@orpc/server'
import { function createORPCClient<T extends AnyNestedClient>(link: ClientLink<InferClientContext<T>>, { path, ...options }?: NoInfer<ORPCClientOptions<T>>): T
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.
@see{@link https://orpc.dev/docs/client/client-side Client-Side Clients}
createORPCClient
} from '@orpc/client'
import { class RPCLink<T extends ClientContext>
Client link that communicates with an RPC Handler over the Fetch API (HTTP).
@see{@link https://orpc.dev/docs/adapters/fetch-api Fetch API Adapter}
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).
@see{@link https://orpc.dev/docs/adapters/fetch-api Fetch API Adapter}
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.
@example'https://api.example.com'@example'http://localhost:3000'
origin
: 'http://127.0.0.1:3000',
RPCLinkCodecOptions<ClientContext>.url?: Value<Promisable<StandardUrl>, [options: ClientOptions<ClientContext>, path: string[], input: unknown]> | undefined
Base url for all requests (without origin). Should match with handler's prefix.
@example'/rpc?base=1'@default'/'
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.
@see{@link https://orpc.dev/docs/client/client-side#creating-a-client Client-Side Clients - Creating a Client}
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.
@see{@link https://orpc.dev/docs/client/client-side Client-Side Clients}
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

Last updated on August 15, 2026

Was this page helpful?