Skip to content

RPC

The effect/unstable/rpc modules let you describe a set of remote procedures once, as schema-backed definitions, and then derive both a typed client and a typed server from that single description. The client method signatures, the server handler signatures, the wire encoding, and the error channel are all generated from the same RpcGroup — so the two ends can never drift out of sync, and a payload or success type change is a compile error rather than a runtime surprise.

import { Schema } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"
// A single shared contract. The client and server both import this group,
// guaranteeing they agree on tags, payloads, results, and errors.
class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()(
"UserNotFound",
{ id: Schema.String }
) {}
export class UserRpcs extends RpcGroup.make(
Rpc.make("GetUser", {
payload: { id: Schema.String }, // what the client sends
success: Schema.Struct({ id: Schema.String, name: Schema.String }),
error: UserNotFound // typed, schema-encoded failure
})
) {}

An RPC definition is just data. Nothing is sent or handled until the group is interpreted: a client reads the schemas to encode requests and decode responses, and a server reads the same schemas to decode requests, run your handlers, and encode results. The transport (HTTP, websocket, socket, worker, or an in-memory test harness) is chosen separately and never affects the typed surface.

RPC is the right tool when you control both ends and want a function-call feel across a process boundary — a backend service consumed by your own frontend, a worker thread, or a child process. It gives you typed payloads, typed errors, streaming results, and middleware without writing any serialization or routing code by hand.

If instead you need a public, REST-shaped HTTP surface with explicit paths, methods, and status codes — for third-party consumers or OpenAPI — reach for the HTTP API modules. RPC and HTTP API share the same Schema and Effect foundations, so the mental model carries over.

  • Defining RPCs — declare a procedure with Rpc.make, attach payload, success, and error schemas, model streaming results, and collect procedures into an RpcGroup.
  • Client and server — implement handlers with group.toLayer, run them with RpcServer, build a typed client with RpcClient, and test the whole thing in-memory with RpcTest.

RPC builds directly on concepts covered elsewhere: Schema for the wire contract, Error Management for typed failures, Services & Layers for wiring, Streaming for streaming RPCs, and the Platform / HTTP Client modules for transports.